diff options
Diffstat (limited to 'activerecord')
61 files changed, 1260 insertions, 536 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index cbbfad615d..e2dc8045e2 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,67 @@ +* Support for PostgreSQL foreign tables. + + *fatkodima* + +* Fix relation merger issue with `left_outer_joins`. + + *Mehmet Emin İNAÇ* + +* Don't allow destroyed object mutation after `save` or `save!` is called. + + *Ryuta Kamizono* + +* Take into account association conditions when deleting through records. + + Fixes #18424. + + *Piotr Jakubowski* + +* Fix nested `has_many :through` associations on unpersisted parent instances. + + For example, if you have + + class Post < ActiveRecord::Base + belongs_to :author + has_many :books, through: :author + has_many :subscriptions, through: :books + end + + class Author < ActiveRecord::Base + has_one :post + has_many :books + has_many :subscriptions, through: :books + end + + class Book < ActiveRecord::Base + belongs_to :author + has_many :subscriptions + end + + class Subscription < ActiveRecord::Base + belongs_to :book + end + + Before: + + If `post` is not persisted, then `post.subscriptions` will be empty. + + After: + + If `post` is not persisted, then `post.subscriptions` can be set and used + just like it would if `post` were persisted. + + Fixes #16313. + + *Zoltan Kiss* + +* Fixed inconsistency with `first(n)` when used with `limit()`. + The `first(n)` finder now respects the `limit()`, making it consistent + with `relation.to_a.first(n)`, and also with the behavior of `last(n)`. + + Fixes #23979. + + *Brian Christian* + * Use `count(:all)` in `HasManyAssociation#count_records` to prevent invalid SQL queries for association counting. diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 661605d3e5..b1cad0d0a4 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1061,12 +1061,6 @@ module ActiveRecord # belongs_to :dungeon, inverse_of: :evil_wizard # end # - # There are limitations to <tt>:inverse_of</tt> support: - # - # * does not work with <tt>:through</tt> associations. - # * does not work with <tt>:polymorphic</tt> associations. - # * inverse associations for #belongs_to associations #has_many are ignored. - # # For more information, see the documentation for the +:inverse_of+ option. # # == Deleting from associations @@ -1279,6 +1273,9 @@ module ActiveRecord # Specify the foreign key used for the association. By default this is guessed to be the name # of this class in lower-case and "_id" suffixed. So a Person class that makes a #has_many # association will use "person_id" as the default <tt>:foreign_key</tt>. + # + # If you are going to modify the association (rather than just read from it), then it is + # a good idea to set the <tt>:inverse_of</tt> option. # [:foreign_type] # Specify the column used to store the associated object's type, if this is a polymorphic # association. By default this is guessed to be the name of the polymorphic association @@ -1352,8 +1349,7 @@ module ActiveRecord # <tt>:autosave</tt> to <tt>true</tt>. # [:inverse_of] # Specifies the name of the #belongs_to association on the associated object - # that is the inverse of this #has_many association. Does not work in combination - # with <tt>:through</tt> or <tt>:as</tt> options. + # that is the inverse of this #has_many association. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. # [:extend] # Specifies a module or array of modules that will be extended into the association object returned. @@ -1449,6 +1445,9 @@ module ActiveRecord # Specify the foreign key used for the association. By default this is guessed to be the name # of this class in lower-case and "_id" suffixed. So a Person class that makes a #has_one association # will use "person_id" as the default <tt>:foreign_key</tt>. + # + # If you are going to modify the association (rather than just read from it), then it is + # a good idea to set the <tt>:inverse_of</tt> option. # [:foreign_type] # Specify the column used to store the associated object's type, if this is a polymorphic # association. By default this is guessed to be the name of the polymorphic association @@ -1464,6 +1463,9 @@ module ActiveRecord # <tt>:primary_key</tt>, and <tt>:foreign_key</tt> are ignored, as the association uses the # source reflection. You can only use a <tt>:through</tt> query through a #has_one # or #belongs_to association on the join model. + # + # If you are going to modify the association (rather than just read from it), then it is + # a good idea to set the <tt>:inverse_of</tt> option. # [:source] # Specifies the source association name used by #has_one <tt>:through</tt> queries. # Only use it if the name cannot be inferred from the association. @@ -1484,8 +1486,7 @@ module ActiveRecord # <tt>:autosave</tt> to <tt>true</tt>. # [:inverse_of] # Specifies the name of the #belongs_to association on the associated object - # that is the inverse of this #has_one association. Does not work in combination - # with <tt>:through</tt> or <tt>:as</tt> options. + # that is the inverse of this #has_one association. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. # [:required] # When set to +true+, the association will also have its presence validated. @@ -1570,6 +1571,9 @@ module ActiveRecord # association will use "person_id" as the default <tt>:foreign_key</tt>. Similarly, # <tt>belongs_to :favorite_person, class_name: "Person"</tt> will use a foreign key # of "favorite_person_id". + # + # If you are going to modify the association (rather than just read from it), then it is + # a good idea to set the <tt>:inverse_of</tt> option. # [:foreign_type] # Specify the column used to store the associated object's type, if this is a polymorphic # association. By default this is guessed to be the name of the association with a "_type" @@ -1619,8 +1623,7 @@ module ActiveRecord # +after_commit+ and +after_rollback+ callbacks are executed. # [:inverse_of] # Specifies the name of the #has_one or #has_many association on the associated - # object that is the inverse of this #belongs_to association. Does not work in - # combination with the <tt>:polymorphic</tt> options. + # object that is the inverse of this #belongs_to association. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. # [:optional] # When set to +true+, the association will not have its presence validated. @@ -1789,6 +1792,9 @@ module ActiveRecord # of this class in lower-case and "_id" suffixed. So a Person class that makes # a #has_and_belongs_to_many association to Project will use "person_id" as the # default <tt>:foreign_key</tt>. + # + # If you are going to modify the association (rather than just read from it), then it is + # a good idea to set the <tt>:inverse_of</tt> option. # [:association_foreign_key] # Specify the foreign key used for the association on the receiving side of the association. # By default this is guessed to be the name of the associated class in lower-case and "_id" suffixed. diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb index 14881cfe17..4f3893588e 100644 --- a/activerecord/lib/active_record/associations/alias_tracker.rb +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -30,20 +30,12 @@ module ActiveRecord join.left.scan( /JOIN(?:\s+\w+)?\s+(?:\S+\s+)?(?:#{quoted_name}|#{name})\sON/i ).size - elsif join.respond_to? :left + elsif join.is_a?(Arel::Nodes::Join) join.left.name == name ? 1 : 0 elsif join.is_a?(Hash) join.fetch(name, 0) else - # this branch is reached by two tests: - # - # activerecord/test/cases/associations/cascaded_eager_loading_test.rb:37 - # with :posts - # - # activerecord/test/cases/associations/eager_test.rb:1133 - # with :comments - # - 0 + raise ArgumentError, "joins list should be initialized by list of Arel::Nodes::Join" end end diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index ca3032d967..7c69cd65ee 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -104,8 +104,8 @@ module ActiveRecord::Associations::Builder # :nodoc: def self.define_readers(mixin, name) mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 - def #{name}(*args) - association(:#{name}).reader(*args) + def #{name} + association(:#{name}).reader end CODE end diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index adbf52b87c..59929b8c4e 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -8,9 +8,7 @@ module ActiveRecord def initialize(owner, reflection) super - - @through_records = {} - @through_association = nil + @through_records = {} end def concat(*records) @@ -50,11 +48,6 @@ module ActiveRecord end private - - def through_association - @through_association ||= owner.association(through_reflection.name) - end - # The through record (built with build_record) is temporarily cached # so that it may be reused if insert_record is subsequently called. # @@ -140,21 +133,15 @@ module ActiveRecord scope = through_association.scope scope.where! construct_join_attributes(*records) + scope = scope.where(through_scope_attributes) case method when :destroy if scope.klass.primary_key - count = scope.destroy_all.length + count = scope.destroy_all.count(&:destroyed?) else scope.each(&:_run_destroy_callbacks) - - arel = scope.arel - - stmt = Arel::DeleteManager.new - stmt.from scope.klass.arel_table - stmt.wheres = arel.constraints - - count = scope.klass.connection.delete(stmt, "SQL") + count = scope.delete_all end when :nullify count = scope.update_all(source_reflection.foreign_key => nil) diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb index 36746f9115..491282adf7 100644 --- a/activerecord/lib/active_record/associations/has_one_through_association.rb +++ b/activerecord/lib/active_record/associations/has_one_through_association.rb @@ -6,17 +6,16 @@ module ActiveRecord class HasOneThroughAssociation < HasOneAssociation #:nodoc: include ThroughAssociation - def replace(record) - create_through_record(record) + def replace(record, save = true) + create_through_record(record, save) self.target = record end private - - def create_through_record(record) + def create_through_record(record, save) ensure_not_nested - through_proxy = owner.association(through_reflection.name) + through_proxy = through_association through_record = through_proxy.load_target if through_record && !record @@ -30,7 +29,7 @@ module ActiveRecord if through_record through_record.update(attributes) - elsif owner.new_record? + elsif owner.new_record? || !save through_proxy.build(attributes) else through_proxy.create(attributes) diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index e1087be9b3..59320431ee 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -83,7 +83,7 @@ module ActiveRecord # { author: :avatar } # [ :books, { author: :avatar } ] def preload(records, associations, preload_scope = nil) - records = records.compact + records = Array.wrap(records).compact if records.empty? [] diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index bce2a95ce1..5afb0bc068 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -4,9 +4,24 @@ module ActiveRecord module Associations # = Active Record Through Association module ThroughAssociation #:nodoc: - delegate :source_reflection, :through_reflection, to: :reflection + delegate :source_reflection, to: :reflection private + def through_reflection + @through_reflection ||= begin + refl = reflection.through_reflection + + while refl.through_reflection? + refl = refl.through_reflection + end + + refl + end + end + + def through_association + @through_association ||= owner.association(through_reflection.name) + end # We merge in these scopes for two reasons: # @@ -38,24 +53,22 @@ module ActiveRecord def construct_join_attributes(*records) ensure_mutable - if source_reflection.association_primary_key(reflection.klass) == reflection.klass.primary_key + association_primary_key = source_reflection.association_primary_key(reflection.klass) + + if association_primary_key == reflection.klass.primary_key && !options[:source_type] join_attributes = { source_reflection.name => records } else join_attributes = { - source_reflection.foreign_key => - records.map { |record| - record.send(source_reflection.association_primary_key(reflection.klass)) - } + source_reflection.foreign_key => records.map(&association_primary_key.to_sym) } end if options[:source_type] - join_attributes[source_reflection.foreign_type] = - records.map { |record| record.class.base_class.name } + join_attributes[source_reflection.foreign_type] = [ options[:source_type] ] end if records.count == 1 - Hash[join_attributes.map { |k, v| [k, v.first] }] + join_attributes.transform_values!(&:first) else join_attributes end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 3de6fe566d..df4c79b0f6 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -32,9 +32,7 @@ module ActiveRecord # <tt>reload</tt> the record and clears changed attributes. def reload(*) super.tap do - @previously_changed = ActiveSupport::HashWithIndifferentAccess.new @mutations_before_last_save = nil - @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new @mutations_from_database = nil end end @@ -114,12 +112,12 @@ module ActiveRecord # Alias for +changed+ def changed_attribute_names_to_save - changes_to_save.keys + mutations_from_database.changed_attribute_names end # Alias for +changed_attributes+ def attributes_in_database - changes_to_save.transform_values(&:first) + mutations_from_database.changed_values end private diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 36048bee03..d663b59444 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -356,35 +356,33 @@ module ActiveRecord # Inserts a set of fixtures into the table. Overridden in adapters that require # something beyond a simple insert (eg. Oracle). def insert_fixtures(fixtures, table_name) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + `insert_fixtures` is deprecated and will be removed in the next version of Rails. + Consider using `insert_fixtures_set` for performance improvement. + MSG return if fixtures.empty? - columns = schema_cache.columns_hash(table_name) + execute(build_fixture_sql(fixtures, table_name), "Fixtures Insert") + end - values = fixtures.map do |fixture| - fixture = fixture.stringify_keys + def insert_fixtures_set(fixture_set, tables_to_delete = []) + fixture_inserts = fixture_set.map do |table_name, fixtures| + next if fixtures.empty? - unknown_columns = fixture.keys - columns.keys - if unknown_columns.any? - raise Fixture::FixtureError, %(table "#{table_name}" has no columns named #{unknown_columns.map(&:inspect).join(', ')}.) - end + build_fixture_sql(fixtures, table_name) + end.compact - columns.map do |name, column| - if fixture.key?(name) - type = lookup_cast_type_from_column(column) - bind = Relation::QueryAttribute.new(name, fixture[name], type) - with_yaml_fallback(bind.value_for_database) - else - Arel.sql("DEFAULT") + table_deletes = tables_to_delete.map { |table| "DELETE FROM #{quote_table_name table}".dup } + total_sql = Array.wrap(combine_multi_statements(table_deletes + fixture_inserts)) + + disable_referential_integrity do + transaction(requires_new: true) do + total_sql.each do |sql| + execute sql, "Fixtures Load" + yield if block_given? end end end - - table = Arel::Table.new(table_name) - manager = Arel::InsertManager.new - manager.into(table) - columns.each_key { |column| manager.columns << table[column] } - manager.values = manager.create_values_list(values) - execute manager.to_sql, "Fixtures Insert" end def empty_insert_statement_value @@ -417,6 +415,41 @@ module ActiveRecord private + def build_fixture_sql(fixtures, table_name) + columns = schema_cache.columns_hash(table_name) + + values = fixtures.map do |fixture| + fixture = fixture.stringify_keys + + unknown_columns = fixture.keys - columns.keys + if unknown_columns.any? + raise Fixture::FixtureError, %(table "#{table_name}" has no columns named #{unknown_columns.map(&:inspect).join(', ')}.) + end + + columns.map do |name, column| + if fixture.key?(name) + type = lookup_cast_type_from_column(column) + bind = Relation::QueryAttribute.new(name, fixture[name], type) + with_yaml_fallback(bind.value_for_database) + else + Arel.sql("DEFAULT") + end + end + end + + table = Arel::Table.new(table_name) + manager = Arel::InsertManager.new + manager.into(table) + columns.each_key { |column| manager.columns << table[column] } + manager.values = manager.create_values_list(values) + + manager.to_sql + end + + def combine_multi_statements(total_sql) + total_sql.join(";\n") + end + # Returns a subquery for the given key using the join information. def subquery_for(key, select) subselect = select.clone 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 4f58b0242c..c32a234be4 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -1049,8 +1049,8 @@ module ActiveRecord sm_table = quote_table_name(ActiveRecord::SchemaMigration.table_name) migrated = ActiveRecord::SchemaMigration.all_versions.map(&:to_i) - versions = ActiveRecord::Migrator.migration_files(migrations_paths).map do |file| - ActiveRecord::Migrator.parse_migration_filename(file).first.to_i + versions = migration_context.migration_files.map do |file| + migration_context.parse_migration_filename(file).first.to_i end unless migrated.include?(version) diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index fc80d332f9..7bd54f777e 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -119,6 +119,14 @@ module ActiveRecord end end + def migrations_paths + @config[:migrations_paths] || Migrator.migrations_paths + end + + def migration_context + MigrationContext.new(migrations_paths) + end + class Version include Comparable @@ -537,12 +545,7 @@ module ActiveRecord end def extract_limit(sql_type) - case sql_type - when /^bigint/i - 8 - when /\((.*)\)/ - $1.to_i - end + $1.to_i if sql_type =~ /\((.*)\)/ end def translate_exception_class(e, sql) 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 0afdd959f5..d1a3b6de40 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -249,7 +249,7 @@ module ActiveRecord # create_database 'matt_development', charset: :big5 def create_database(name, options = {}) if options[:collation] - execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset] || 'utf8')} COLLATE #{quote_table_name(options[:collation])}" + execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT COLLATE #{quote_table_name(options[:collation])}" else execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset] || 'utf8')}" end @@ -530,8 +530,56 @@ module ActiveRecord without_sql_mode("NO_AUTO_VALUE_ON_ZERO") { super } end + def insert_fixtures_set(fixture_set, tables_to_delete = []) + iterate_over_results = -> { while raw_connection.next_result; end; } + + with_multi_statements do + without_sql_mode("NO_AUTO_VALUE_ON_ZERO") do + super(fixture_set, tables_to_delete, &iterate_over_results) + end + end + end + private + def combine_multi_statements(total_sql) + total_sql.each_with_object([]) do |sql, total_sql_chunks| + previous_packet = total_sql_chunks.last + sql << ";\n" + if max_allowed_packet_reached?(sql, previous_packet) || total_sql_chunks.empty? + total_sql_chunks << sql + else + previous_packet << sql + end + end + end + + def max_allowed_packet_reached?(current_packet, previous_packet) + if current_packet.bytesize > max_allowed_packet + raise ActiveRecordError, "Fixtures set is too large #{current_packet.bytesize}. Consider increasing the max_allowed_packet variable." + elsif previous_packet.nil? + false + else + (current_packet.bytesize + previous_packet.bytesize) > max_allowed_packet + end + end + + def max_allowed_packet + bytes_margin = 2 + @max_allowed_packet ||= (show_variable("max_allowed_packet") - bytes_margin) + end + + def with_multi_statements + previous_flags = @config[:flags] + @config[:flags] = Mysql2::Client::MULTI_STATEMENTS + reconnect! + + yield + ensure + @config[:flags] = previous_flags + reconnect! + end + def without_sql_mode(mode) result = execute("SELECT @@SESSION.sql_mode") current_mode = result.first[0] diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb index a89aa5ea09..6edb7cfd3c 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb @@ -60,7 +60,7 @@ module ActiveRecord end def type_cast_single_for_database(value) - infinity?(value) ? "" : @subtype.serialize(value) + infinity?(value) ? value : @subtype.serialize(value) end def extract_bounds(value) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index 9fdeab06c1..e75202b0be 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -138,7 +138,7 @@ module ActiveRecord end def encode_range(range) - "[#{type_cast(range.first)},#{type_cast(range.last)}#{range.exclude_end? ? ')' : ']'}" + "[#{type_cast_range_value(range.first)},#{type_cast_range_value(range.last)}#{range.exclude_end? ? ')' : ']'}" end def determine_encoding_of_strings_in_array(value) @@ -154,6 +154,14 @@ module ActiveRecord else _type_cast(values) end end + + def type_cast_range_value(value) + infinity?(value) ? "" : type_cast(value) + end + + def infinity?(value) + value.respond_to?(:infinite?) && value.infinite? + end end end 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 5e7bd4c871..8678fab2ac 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -38,7 +38,7 @@ module ActiveRecord " TABLESPACE = \"#{value}\"" when :connection_limit " CONNECTION LIMIT = #{value}" - else + else "" end end @@ -527,6 +527,14 @@ module ActiveRecord end end + def foreign_tables + query_values(data_source_sql(type: "FOREIGN TABLE"), "SCHEMA") + end + + def foreign_table_exists?(table_name) + query_values(data_source_sql(table_name, type: "FOREIGN TABLE"), "SCHEMA").any? if table_name.present? + end + # Maps logical Rails types to PostgreSQL-specific data types. def type_to_sql(type, limit: nil, precision: nil, scale: nil, array: nil, **) # :nodoc: sql = \ @@ -739,7 +747,7 @@ module ActiveRecord def data_source_sql(name = nil, type: nil) scope = quoted_scope(name, type: type) - scope[:type] ||= "'r','v','m'" # (r)elation/table, (v)iew, (m)aterialized view + scope[:type] ||= "'r','v','m','f'" # (r)elation/table, (v)iew, (m)aterialized view, (f)oreign table sql = "SELECT c.relname FROM pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace".dup sql << " WHERE n.nspname = #{scope[:schema]}" @@ -756,6 +764,8 @@ module ActiveRecord "'r'" when "VIEW" "'v','m'" + when "FOREIGN TABLE" + "'f'" end scope = {} scope[:schema] = schema ? quote(schema) : "ANY (current_schemas(false))" diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 441be47fa1..ddc5a91286 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Make sure we're using pg high enough for type casts and Ruby 2.2+ compatibility -gem "pg", "~> 0.18" +gem "pg", ">= 0.18", "< 2.0" require "pg" require "active_record/connection_adapters/abstract_adapter" @@ -318,6 +318,10 @@ module ActiveRecord postgresql_version >= 90300 end + def supports_foreign_tables? + postgresql_version >= 90300 + end + def supports_pgcrypto_uuid? postgresql_version >= 90400 end diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index f34b6733da..c29cf1f9a1 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -28,7 +28,7 @@ module ActiveRecord coder["columns_hash"] = @columns_hash coder["primary_keys"] = @primary_keys coder["data_sources"] = @data_sources - coder["version"] = ActiveRecord::Migrator.current_version + coder["version"] = connection.migration_context.current_version end def init_with(coder) @@ -100,7 +100,7 @@ module ActiveRecord def marshal_dump # if we get current version during initialization, it happens stack over flow. - @version = ActiveRecord::Migrator.current_version + @version = connection.migration_context.current_version [@version, @columns, @columns_hash, @primary_keys, @data_sources] end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 441c7cd28f..c66cada07a 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -101,7 +101,7 @@ module ActiveRecord def initialize(connection, logger, connection_options, config) super(connection, logger, config) - @active = nil + @active = true @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit])) configure_connection @@ -144,7 +144,7 @@ module ActiveRecord end def active? - @active != false + @active end # Disconnects from the database if already connected. Otherwise, this @@ -290,19 +290,18 @@ module ActiveRecord rename_table_indexes(table_name, new_name) end - # See: https://www.sqlite.org/lang_altertable.html - # SQLite has an additional restriction on the ALTER TABLE statement - def valid_alter_table_type?(type) - type.to_sym != :primary_key + def valid_alter_table_type?(type, options = {}) + !invalid_alter_table_type?(type, options) end + deprecate :valid_alter_table_type? def add_column(table_name, column_name, type, options = {}) #:nodoc: - if valid_alter_table_type?(type) && !options[:primary_key] - super(table_name, column_name, type, options) - else + if invalid_alter_table_type?(type, options) alter_table(table_name) do |definition| definition.column(column_name, type, options) end + else + super end end @@ -373,6 +372,18 @@ module ActiveRecord end end + def insert_fixtures_set(fixture_set, tables_to_delete = []) + disable_referential_integrity do + transaction(requires_new: true) do + tables_to_delete.each { |table| delete "DELETE FROM #{quote_table_name(table)}", "Fixture Delete" } + + fixture_set.each do |table_name, rows| + rows.each { |row| insert_fixture(row, table_name) } + end + end + end + end + private def initialize_type_map(m = type_map) super @@ -386,6 +397,12 @@ module ActiveRecord end alias column_definitions table_structure + # See: https://www.sqlite.org/lang_altertable.html + # SQLite has an additional restriction on the ALTER TABLE statement + def invalid_alter_table_type?(type, options) + type.to_sym == :primary_key || options[:primary_key] + end + def alter_table(table_name, options = {}) altered_table_name = "a#{table_name}" caller = lambda { |definition| yield definition if block_given? } diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 86f13d75d5..896d51c0fe 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -540,47 +540,38 @@ module ActiveRecord } unless files_to_read.empty? - connection.disable_referential_integrity do - fixtures_map = {} - - fixture_sets = files_to_read.map do |fs_name| - klass = class_names[fs_name] - conn = klass ? klass.connection : connection - fixtures_map[fs_name] = new( # ActiveRecord::FixtureSet.new - conn, - fs_name, - klass, - ::File.join(fixtures_directory, fs_name)) - end - - update_all_loaded_fixtures fixtures_map - - connection.transaction(requires_new: true) do - deleted_tables = Hash.new { |h, k| h[k] = Set.new } - fixture_sets.each do |fs| - conn = fs.model_class.respond_to?(:connection) ? fs.model_class.connection : connection - table_rows = fs.table_rows + fixtures_map = {} + + fixture_sets = files_to_read.map do |fs_name| + klass = class_names[fs_name] + conn = klass ? klass.connection : connection + fixtures_map[fs_name] = new( # ActiveRecord::FixtureSet.new + conn, + fs_name, + klass, + ::File.join(fixtures_directory, fs_name)) + end - table_rows.each_key do |table| - unless deleted_tables[conn].include? table - conn.delete "DELETE FROM #{conn.quote_table_name(table)}", "Fixture Delete" - end - deleted_tables[conn] << table - end + update_all_loaded_fixtures fixtures_map + fixture_sets_by_connection = fixture_sets.group_by { |fs| fs.model_class ? fs.model_class.connection : connection } - table_rows.each do |fixture_set_name, rows| - conn.insert_fixtures(rows, fixture_set_name) - end + fixture_sets_by_connection.each do |conn, set| + table_rows_for_connection = Hash.new { |h, k| h[k] = [] } - # Cap primary key sequences to max(pk). - if conn.respond_to?(:reset_pk_sequence!) - conn.reset_pk_sequence!(fs.table_name) - end + set.each do |fs| + fs.table_rows.each do |table, rows| + table_rows_for_connection[table].unshift(*rows) end end + conn.insert_fixtures_set(table_rows_for_connection, table_rows_for_connection.keys) - cache_fixtures(connection, fixtures_map) + # Cap primary key sequences to max(pk). + if conn.respond_to?(:reset_pk_sequence!) + set.each { |fs| conn.reset_pk_sequence!(fs.table_name) } + end end + + cache_fixtures(connection, fixtures_map) end cached_fixtures(connection, fixture_set_names) end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index f6648a4e3d..798328233f 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -3,6 +3,7 @@ require "set" require "zlib" require "active_support/core_ext/module/attribute_accessors" +require "active_record/tasks/database_tasks" module ActiveRecord class MigrationError < ActiveRecordError#:nodoc: @@ -550,7 +551,7 @@ module ActiveRecord end def call(env) - mtime = ActiveRecord::Migrator.last_migration.mtime.to_i + mtime = ActiveRecord::Base.connection.migration_context.last_migration.mtime.to_i if @last_check < mtime ActiveRecord::Migration.check_pending!(connection) @last_check = mtime @@ -575,11 +576,11 @@ module ActiveRecord # Raises <tt>ActiveRecord::PendingMigrationError</tt> error if any migrations are pending. def check_pending!(connection = Base.connection) - raise ActiveRecord::PendingMigrationError if ActiveRecord::Migrator.needs_migration?(connection) + raise ActiveRecord::PendingMigrationError if connection.migration_context.needs_migration? end def load_schema_if_pending! - if ActiveRecord::Migrator.needs_migration? || !ActiveRecord::Migrator.any_migrations? + if Base.connection.migration_context.needs_migration? || !Base.connection.migration_context.any_migrations? # Roundtrip to Rake to allow plugins to hook into database initialization. root = defined?(ENGINE_ROOT) ? ENGINE_ROOT : Rails.root FileUtils.cd(root) do @@ -876,10 +877,10 @@ module ActiveRecord FileUtils.mkdir_p(destination) unless File.exist?(destination) - destination_migrations = ActiveRecord::Migrator.migrations(destination) + destination_migrations = ActiveRecord::MigrationContext.new(destination).migrations last = destination_migrations.last sources.each do |scope, path| - source_migrations = ActiveRecord::Migrator.migrations(path) + source_migrations = ActiveRecord::MigrationContext.new(path).migrations source_migrations.each do |migration| source = File.binread(migration.filename) @@ -997,132 +998,147 @@ module ActiveRecord end end - class Migrator#:nodoc: - class << self - attr_writer :migrations_paths - alias :migrations_path= :migrations_paths= - - def migrate(migrations_paths, target_version = nil, &block) - case - when target_version.nil? - up(migrations_paths, target_version, &block) - when current_version == 0 && target_version == 0 - [] - when current_version > target_version - down(migrations_paths, target_version, &block) - else - up(migrations_paths, target_version, &block) - end - end + class MigrationContext # :nodoc: + attr_reader :migrations_paths - def rollback(migrations_paths, steps = 1) - move(:down, migrations_paths, steps) - end + def initialize(migrations_paths) + @migrations_paths = migrations_paths + end - def forward(migrations_paths, steps = 1) - move(:up, migrations_paths, steps) + def migrate(target_version = nil, &block) + case + when target_version.nil? + up(target_version, &block) + when current_version == 0 && target_version == 0 + [] + when current_version > target_version + down(target_version, &block) + else + up(target_version, &block) end + end - def up(migrations_paths, target_version = nil) - migrations = migrations(migrations_paths) - migrations.select! { |m| yield m } if block_given? + def rollback(steps = 1) + move(:down, steps) + end - new(:up, migrations, target_version).migrate + def forward(steps = 1) + move(:up, steps) + end + + def up(target_version = nil) + selected_migrations = if block_given? + migrations.select { |m| yield m } + else + migrations end - def down(migrations_paths, target_version = nil) - migrations = migrations(migrations_paths) - migrations.select! { |m| yield m } if block_given? + Migrator.new(:up, selected_migrations, target_version).migrate + end - new(:down, migrations, target_version).migrate + def down(target_version = nil) + selected_migrations = if block_given? + migrations.select { |m| yield m } + else + migrations end - def run(direction, migrations_paths, target_version) - new(direction, migrations(migrations_paths), target_version).run - end + Migrator.new(:down, selected_migrations, target_version).migrate + end - def open(migrations_paths) - new(:up, migrations(migrations_paths), nil) - end + def run(direction, target_version) + Migrator.new(direction, migrations, target_version).run + end - def get_all_versions - if SchemaMigration.table_exists? - SchemaMigration.all_versions.map(&:to_i) - else - [] - end - end + def open + Migrator.new(:up, migrations, nil) + end - def current_version(connection = nil) - get_all_versions.max || 0 - rescue ActiveRecord::NoDatabaseError + def get_all_versions + if SchemaMigration.table_exists? + SchemaMigration.all_versions.map(&:to_i) + else + [] end + end - def needs_migration?(connection = nil) - (migrations(migrations_paths).collect(&:version) - get_all_versions).size > 0 - end + def current_version + get_all_versions.max || 0 + rescue ActiveRecord::NoDatabaseError + end - def any_migrations? - migrations(migrations_paths).any? - end + def needs_migration? + (migrations.collect(&:version) - get_all_versions).size > 0 + end - def last_migration #:nodoc: - migrations(migrations_paths).last || NullMigration.new - end + def any_migrations? + migrations.any? + end - def migrations_paths - @migrations_paths ||= ["db/migrate"] - # just to not break things if someone uses: migrations_path = some_string - Array(@migrations_paths) - end + def last_migration #:nodoc: + migrations.last || NullMigration.new + end - def parse_migration_filename(filename) # :nodoc: - File.basename(filename).scan(Migration::MigrationFilenameRegexp).first + def parse_migration_filename(filename) # :nodoc: + File.basename(filename).scan(Migration::MigrationFilenameRegexp).first + end + + def migrations + migrations = migration_files.map do |file| + version, name, scope = parse_migration_filename(file) + raise IllegalMigrationNameError.new(file) unless version + version = version.to_i + name = name.camelize + + MigrationProxy.new(name, version, file, scope) end - def migrations(paths) - paths = Array(paths) + migrations.sort_by(&:version) + end - migrations = migration_files(paths).map do |file| - version, name, scope = parse_migration_filename(file) - raise IllegalMigrationNameError.new(file) unless version - version = version.to_i - name = name.camelize + def migrations_status + db_list = ActiveRecord::SchemaMigration.normalized_versions - MigrationProxy.new(name, version, file, scope) - end + file_list = migration_files.map do |file| + version, name, scope = parse_migration_filename(file) + raise IllegalMigrationNameError.new(file) unless version + version = ActiveRecord::SchemaMigration.normalize_migration_number(version) + status = db_list.delete(version) ? "up" : "down" + [status, version, (name + scope).humanize] + end.compact - migrations.sort_by(&:version) + db_list.map! do |version| + ["up", version, "********** NO FILE **********"] end - def migrations_status(paths) - paths = Array(paths) - - db_list = ActiveRecord::SchemaMigration.normalized_versions + (db_list + file_list).sort_by { |_, version, _| version } + end - file_list = migration_files(paths).map do |file| - version, name, scope = parse_migration_filename(file) - raise IllegalMigrationNameError.new(file) unless version - version = ActiveRecord::SchemaMigration.normalize_migration_number(version) - status = db_list.delete(version) ? "up" : "down" - [status, version, (name + scope).humanize] - end.compact + def migration_files + paths = Array(migrations_paths) + Dir[*paths.flat_map { |path| "#{path}/**/[0-9]*_*.rb" }] + end - db_list.map! do |version| - ["up", version, "********** NO FILE **********"] - end + def current_environment + ActiveRecord::ConnectionHandling::DEFAULT_ENV.call + end - (db_list + file_list).sort_by { |_, version, _| version } - end + def protected_environment? + ActiveRecord::Base.protected_environments.include?(last_stored_environment) if last_stored_environment + end - def migration_files(paths) - Dir[*paths.flat_map { |path| "#{path}/**/[0-9]*_*.rb" }] - end + def last_stored_environment + return nil if current_version == 0 + raise NoEnvironmentInSchemaError unless ActiveRecord::InternalMetadata.table_exists? - private + environment = ActiveRecord::InternalMetadata[:environment] + raise NoEnvironmentInSchemaError unless environment + environment + end - def move(direction, migrations_paths, steps) - migrator = new(direction, migrations(migrations_paths)) + private + def move(direction, steps) + migrator = Migrator.new(direction, migrations) if current_version != 0 && !migrator.current_migration raise UnknownMigrationVersionError.new(current_version) @@ -1137,10 +1153,29 @@ module ActiveRecord finish = migrator.migrations[start_index + steps] version = finish ? finish.version : 0 - send(direction, migrations_paths, version) + send(direction, version) + end + end + + class Migrator # :nodoc: + class << self + attr_accessor :migrations_paths + + def migrations_path=(path) + ActiveSupport::Deprecation.warn \ + "ActiveRecord::Migrator.migrations_paths= is now deprecated and will be removed in Rails 6.0." \ + "You can set the `migrations_paths` on the `connection` instead through the `database.yml`." + self.migrations_paths = [path] + end + + # For cases where a table doesn't exist like loading from schema cache + def current_version + MigrationContext.new(migrations_paths).current_version end end + self.migrations_paths = ["db/migrate"] + def initialize(direction, migrations, target_version = nil) @direction = direction @target_version = target_version @@ -1203,7 +1238,7 @@ module ActiveRecord end def load_migrated - @migrated_versions = Set.new(self.class.get_all_versions) + @migrated_versions = Set.new(Base.connection.migration_context.get_all_versions) end private @@ -1235,7 +1270,7 @@ module ActiveRecord # Stores the current environment in the database. def record_environment return if down? - ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment + ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Base.connection.migration_context.current_environment end def ran?(migration) @@ -1294,23 +1329,6 @@ module ActiveRecord end end - def self.last_stored_environment - return nil if current_version == 0 - raise NoEnvironmentInSchemaError unless ActiveRecord::InternalMetadata.table_exists? - - environment = ActiveRecord::InternalMetadata[:environment] - raise NoEnvironmentInSchemaError unless environment - environment - end - - def self.current_environment - ActiveRecord::ConnectionHandling::DEFAULT_ENV.call - end - - def self.protected_environment? - ActiveRecord::Base.protected_environments.include?(last_stored_environment) if last_stored_environment - end - def up? @direction == :up end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 462e5e7aaf..a45d011d75 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -359,10 +359,10 @@ module ActiveRecord # Any change to the attributes on either instance will affect both instances. # If you want to change the sti column as well, use #becomes! instead. def becomes(klass) - became = klass.new + became = klass.allocate + became.send(:initialize) became.instance_variable_set("@attributes", @attributes) became.instance_variable_set("@mutations_from_database", @mutations_from_database) if defined?(@mutations_from_database) - became.instance_variable_set("@changed_attributes", attributes_changed_by_setter) became.instance_variable_set("@new_record", new_record?) became.instance_variable_set("@destroyed", destroyed?) became.errors.copy!(errors) @@ -694,6 +694,7 @@ module ActiveRecord def create_or_update(*args, &block) _raise_readonly_record_error if readonly? + return false if destroyed? result = new_record? ? _create_record(&block) : _update_record(*args, &block) result != false end diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 4538ed6a5f..7b4b59ac4b 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -91,6 +91,7 @@ module ActiveRecord if File.file?(filename) current_version = ActiveRecord::Migrator.current_version + next if current_version.nil? cache = YAML.load(File.read(filename)) diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index fce3e1c5cf..2e55713311 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -6,7 +6,7 @@ db_namespace = namespace :db do desc "Set the environment value for the database" task "environment:set" => :load_config do ActiveRecord::InternalMetadata.create_table - ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment + ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Base.connection.migration_context.current_environment end task check_protected_environments: :load_config do @@ -99,9 +99,8 @@ db_namespace = namespace :db do ActiveRecord::Tasks::DatabaseTasks.check_target_version - ActiveRecord::Migrator.run( + ActiveRecord::Base.connection.migration_context.run( :up, - ActiveRecord::Tasks::DatabaseTasks.migrations_paths, ActiveRecord::Tasks::DatabaseTasks.target_version ) db_namespace["_dump"].invoke @@ -113,9 +112,8 @@ db_namespace = namespace :db do ActiveRecord::Tasks::DatabaseTasks.check_target_version - ActiveRecord::Migrator.run( + ActiveRecord::Base.connection.migration_context.run( :down, - ActiveRecord::Tasks::DatabaseTasks.migrations_paths, ActiveRecord::Tasks::DatabaseTasks.target_version ) db_namespace["_dump"].invoke @@ -131,8 +129,7 @@ db_namespace = namespace :db do puts "\ndatabase: #{ActiveRecord::Base.connection_config[:database]}\n\n" puts "#{'Status'.center(8)} #{'Migration ID'.ljust(14)} Migration Name" puts "-" * 50 - paths = ActiveRecord::Tasks::DatabaseTasks.migrations_paths - ActiveRecord::Migrator.migrations_status(paths).each do |status, version, name| + ActiveRecord::Base.connection.migration_context.migrations_status.each do |status, version, name| puts "#{status.center(8)} #{version.ljust(14)} #{name}" end puts @@ -142,14 +139,14 @@ db_namespace = namespace :db do desc "Rolls the schema back to the previous version (specify steps w/ STEP=n)." task rollback: :load_config do step = ENV["STEP"] ? ENV["STEP"].to_i : 1 - ActiveRecord::Migrator.rollback(ActiveRecord::Tasks::DatabaseTasks.migrations_paths, step) + ActiveRecord::Base.connection.migration_context.rollback(step) db_namespace["_dump"].invoke end # desc 'Pushes the schema to the next version (specify steps w/ STEP=n).' task forward: :load_config do step = ENV["STEP"] ? ENV["STEP"].to_i : 1 - ActiveRecord::Migrator.forward(ActiveRecord::Tasks::DatabaseTasks.migrations_paths, step) + ActiveRecord::Base.connection.migration_context.forward(step) db_namespace["_dump"].invoke end @@ -172,12 +169,12 @@ db_namespace = namespace :db do desc "Retrieves the current schema version number" task version: :load_config do - puts "Current version: #{ActiveRecord::Migrator.current_version}" + puts "Current version: #{ActiveRecord::Base.connection.migration_context.current_version}" end # desc "Raises an error if there are pending migrations" task abort_if_pending_migrations: :load_config do - pending_migrations = ActiveRecord::Migrator.open(ActiveRecord::Tasks::DatabaseTasks.migrations_paths).pending_migrations + pending_migrations = ActiveRecord::Base.connection.migration_context.open.pending_migrations if pending_migrations.any? puts "You have #{pending_migrations.size} pending #{pending_migrations.size > 1 ? 'migrations:' : 'migration:'}" diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index a3f8bfd1f1..c28e31a3da 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -34,7 +34,8 @@ module ActiveRecord def self.add_reflection(ar, name, reflection) ar.clear_reflections_cache - ar._reflections = ar._reflections.merge(name.to_s => reflection) + name = name.to_s + ar._reflections = ar._reflections.except(name).merge!(name => reflection) end def self.add_aggregate_reflection(ar, name, reflection) @@ -563,7 +564,7 @@ module ActiveRecord end VALID_AUTOMATIC_INVERSE_MACROS = [:has_many, :has_one, :belongs_to] - INVALID_AUTOMATIC_INVERSE_OPTIONS = [:conditions, :through, :foreign_key] + INVALID_AUTOMATIC_INVERSE_OPTIONS = [:through, :foreign_key] def add_as_source(seed) seed diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 4df3864d07..10be583ef4 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -430,7 +430,7 @@ module ActiveRecord relation = self if eager_loading? - find_with_associations { |rel, _| relation = rel } + apply_join_dependency { |rel, _| relation = rel } end conn = klass.connection @@ -533,7 +533,7 @@ module ActiveRecord skip_query_cache_if_necessary do @records = if eager_loading? - find_with_associations do |relation, join_dependency| + apply_join_dependency do |relation, join_dependency| if ActiveRecord::NullRelation === relation [] else diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index ff06ecbee1..5f959af5dc 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -148,7 +148,7 @@ module ActiveRecord # # [#<Person id:4>, #<Person id:3>, #<Person id:2>] def last(limit = nil) - return find_last(limit) if loaded? || limit_value + return find_last(limit) if loaded? || has_limit_or_offset? result = ordered_relation.limit(limit) result = result.reverse_order! @@ -313,7 +313,7 @@ module ActiveRecord return false if !conditions || limit_value == 0 if eager_loading? - relation = apply_join_dependency(construct_join_dependency(eager_loading: false)) + relation = apply_join_dependency(eager_loading: false) return relation.exists?(conditions) end @@ -358,24 +358,6 @@ module ActiveRecord offset_value || 0 end - def find_with_associations - # NOTE: the JoinDependency constructed here needs to know about - # any joins already present in `self`, so pass them in - # - # failing to do so means that in cases like activerecord/test/cases/associations/inner_join_association_test.rb:136 - # incorrect SQL is generated. In that case, the join dependency for - # SpecialCategorizations is constructed without knowledge of the - # preexisting join in joins_values to categorizations (by way of - # the `has_many :through` for categories). - # - join_dependency = construct_join_dependency - - relation = apply_join_dependency(join_dependency) - relation._select!(join_dependency.aliases.columns) - - yield relation, join_dependency - end - def construct_relation_for_exists(conditions) relation = except(:select, :distinct, :order)._select!(ONE_AS_ONE).limit!(1) @@ -391,22 +373,29 @@ module ActiveRecord def construct_join_dependency(eager_loading: true) including = eager_load_values + includes_values + joins = joins_values.select { |join| join.is_a?(Arel::Nodes::Join) } ActiveRecord::Associations::JoinDependency.new( - klass, table, including, alias_tracker(joins_values), eager_loading: eager_loading + klass, table, including, alias_tracker(joins), eager_loading: eager_loading ) end - def apply_join_dependency(join_dependency = construct_join_dependency) + def apply_join_dependency(eager_loading: true) + join_dependency = construct_join_dependency(eager_loading: eager_loading) relation = except(:includes, :eager_load, :preload).joins!(join_dependency) - if using_limitable_reflections?(join_dependency.reflections) - relation - else - if relation.limit_value + if eager_loading && !using_limitable_reflections?(join_dependency.reflections) + if has_limit_or_offset? limited_ids = limited_ids_for(relation) limited_ids.empty? ? relation.none! : relation.where!(primary_key => limited_ids) end - relation.except(:limit, :offset) + relation.limit_value = relation.offset_value = nil + end + + if block_given? + relation._select!(join_dependency.aliases.columns) + yield relation, join_dependency + else + relation end end @@ -532,7 +521,11 @@ module ActiveRecord else relation = ordered_relation - if limit_value.nil? || index < limit_value + if limit_value + limit = [limit_value - index, limit].min + end + + if limit > 0 relation = relation.offset(offset_index + index) unless index.zero? relation.limit(limit).to_a else @@ -547,12 +540,11 @@ module ActiveRecord else relation = ordered_relation - relation.to_a[-index] - # TODO: can be made more performant on large result sets by - # for instance, last(index)[-index] (which would require - # refactoring the last(n) finder method to make test suite pass), - # or by using a combination of reverse_order, limit, and offset, - # e.g., reverse_order.offset(index-1).first + if equal?(relation) || has_limit_or_offset? + relation.records[-index] + else + relation.last(index)[-index] + end end end diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index b736b21525..ebdd4144bb 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -52,7 +52,7 @@ module ActiveRecord NORMAL_VALUES = Relation::VALUE_METHODS - Relation::CLAUSE_METHODS - - [:includes, :preload, :joins, :order, :reverse_order, :lock, :create_with, :reordering] # :nodoc: + [:includes, :preload, :joins, :left_outer_joins, :order, :reverse_order, :lock, :create_with, :reordering] # :nodoc: def normal_values NORMAL_VALUES @@ -79,6 +79,7 @@ module ActiveRecord merge_clauses merge_preloads merge_joins + merge_outer_joins relation end @@ -129,6 +130,29 @@ module ActiveRecord end end + def merge_outer_joins + return if other.left_outer_joins_values.blank? + + if other.klass == relation.klass + relation.left_outer_joins!(*other.left_outer_joins_values) + else + alias_tracker = nil + joins_dependency = other.left_outer_joins_values.map do |join| + case join + when Hash, Symbol, Array + alias_tracker ||= other.alias_tracker + ActiveRecord::Associations::JoinDependency.new( + other.klass, other.table, join, alias_tracker + ) + else + join + end + end + + relation.left_outer_joins!(*joins_dependency) + end + end + def merge_multi_values if other.reordering_value # override any order specified in the original relation diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 0296101f81..86882c7ce7 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -327,8 +327,8 @@ module ActiveRecord end VALID_UNSCOPING_VALUES = Set.new([:where, :select, :group, :order, :lock, - :limit, :offset, :joins, :includes, :from, - :readonly, :having]) + :limit, :offset, :joins, :left_outer_joins, + :includes, :from, :readonly, :having]) # Removes an unwanted relation that is already defined on a chain of relations. # This is useful when passing around chains of relations and would like to @@ -375,10 +375,11 @@ module ActiveRecord args.each do |scope| case scope when Symbol + scope = :left_outer_joins if scope == :left_joins if !VALID_UNSCOPING_VALUES.include?(scope) raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}." end - set_value(scope, nil) + set_value(scope, DEFAULT_VALUES[scope]) when Hash scope.each do |key, target_value| if key != :where @@ -905,7 +906,7 @@ module ActiveRecord protected # Returns a relation value with a given name def get_value(name) # :nodoc: - @values[name] || default_value_for(name) + @values.fetch(name, DEFAULT_VALUES[name]) end # Sets the relation value with the given name @@ -978,6 +979,8 @@ module ActiveRecord case join when Hash, Symbol, Array :association_join + when ActiveRecord::Associations::JoinDependency + :stashed_join else raise ArgumentError, "only Hash, Symbol and Array are allowed" end @@ -1013,7 +1016,7 @@ module ActiveRecord join_nodes = buckets[:join_node].uniq string_joins = buckets[:string_join].map(&:strip).uniq - join_list = join_nodes + convert_join_strings_to_ast(manager, string_joins) + join_list = join_nodes + convert_join_strings_to_ast(string_joins) alias_tracker = alias_tracker(join_list, aliases) join_dependency = ActiveRecord::Associations::JoinDependency.new( @@ -1028,7 +1031,7 @@ module ActiveRecord alias_tracker.aliases end - def convert_join_strings_to_ast(table, joins) + def convert_join_strings_to_ast(joins) joins .flatten .reject(&:blank?) @@ -1190,7 +1193,6 @@ module ActiveRecord DEFAULT_VALUES = { create_with: FROZEN_EMPTY_HASH, - readonly: false, where: Relation::WhereClause.empty, having: Relation::WhereClause.empty, from: Relation::FromClause.empty @@ -1199,15 +1201,5 @@ module ActiveRecord Relation::MULTI_VALUE_METHODS.each do |value| DEFAULT_VALUES[value] ||= FROZEN_EMPTY_ARRAY end - - Relation::SINGLE_VALUE_METHODS.each do |value| - DEFAULT_VALUES[value] = nil if DEFAULT_VALUES[value].nil? - end - - def default_value_for(name) - DEFAULT_VALUES.fetch(name) do - raise ArgumentError, "unknown relation value #{name.inspect}" - end - end end end diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb index 1e121f2a09..216359867c 100644 --- a/activerecord/lib/active_record/schema.rb +++ b/activerecord/lib/active_record/schema.rb @@ -55,7 +55,7 @@ module ActiveRecord end ActiveRecord::InternalMetadata.create_table - ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment + ActiveRecord::InternalMetadata[:environment] = connection.migration_context.current_environment end private diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 16ccba6b6c..b8d848b999 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -44,7 +44,7 @@ module ActiveRecord def initialize(connection, options = {}) @connection = connection - @version = Migrator::current_version rescue nil + @version = connection.migration_context.current_version rescue nil @options = options end diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 4657e51e6d..ed589f9b43 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -54,10 +54,10 @@ module ActiveRecord def check_protected_environments! unless ENV["DISABLE_DATABASE_ENVIRONMENT_CHECK"] - current = ActiveRecord::Migrator.current_environment - stored = ActiveRecord::Migrator.last_stored_environment + current = ActiveRecord::Base.connection.migration_context.current_environment + stored = ActiveRecord::Base.connection.migration_context.last_stored_environment - if ActiveRecord::Migrator.protected_environment? + if ActiveRecord::Base.connection.migration_context.protected_environment? raise ActiveRecord::ProtectedEnvironmentError.new(stored) end @@ -169,7 +169,7 @@ module ActiveRecord verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] != "false" : true scope = ENV["SCOPE"] verbose_was, Migration.verbose = Migration.verbose, verbose - Migrator.migrate(migrations_paths, target_version) do |migration| + Base.connection.migration_context.migrate(target_version) do |migration| scope.blank? || scope == migration.scope end ActiveRecord::Base.clear_cache! diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb index 6931b085a8..9ae2c42368 100644 --- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb @@ -108,7 +108,7 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase def test_create_mysql_database_with_encoding assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8`", create_database(:matt) assert_equal "CREATE DATABASE `aimonetti` DEFAULT CHARACTER SET `latin1`", create_database(:aimonetti, charset: "latin1") - assert_equal "CREATE DATABASE `matt_aimonetti` DEFAULT CHARACTER SET `big5` COLLATE `big5_chinese_ci`", create_database(:matt_aimonetti, charset: :big5, collation: :big5_chinese_ci) + assert_equal "CREATE DATABASE `matt_aimonetti` DEFAULT COLLATE `utf8mb4_bin`", create_database(:matt_aimonetti, collation: "utf8mb4_bin") end def test_recreate_mysql_database_with_encoding diff --git a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb index 62abd694bb..d7d9a2d732 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb @@ -36,7 +36,7 @@ class SchemaMigrationsTest < ActiveRecord::Mysql2TestCase assert connection.column_exists?(table_name, :key, :string) end ensure - ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment + ActiveRecord::InternalMetadata[:environment] = connection.migration_context.current_environment end private diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb index 0e9e86f425..7eecd5bc35 100644 --- a/activerecord/test/cases/adapters/postgresql/array_test.rb +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -228,7 +228,9 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase def test_insert_fixtures tag_values = ["val1", "val2", "val3_with_'_multiple_quote_'_chars"] - @connection.insert_fixtures([{ "tags" => tag_values }], "pg_arrays") + assert_deprecated do + @connection.insert_fixtures([{ "tags" => tag_values }], "pg_arrays") + end assert_equal(PgArray.last.tags, tag_values) end diff --git a/activerecord/test/cases/adapters/postgresql/foreign_table_test.rb b/activerecord/test/cases/adapters/postgresql/foreign_table_test.rb new file mode 100644 index 0000000000..4fa315ad23 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/foreign_table_test.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/professor" + +if ActiveRecord::Base.connection.supports_foreign_tables? + class ForeignTableTest < ActiveRecord::TestCase + self.use_transactional_tests = false + + class ForeignProfessor < ActiveRecord::Base + self.table_name = "foreign_professors" + end + + class ForeignProfessorWithPk < ForeignProfessor + self.primary_key = "id" + end + + def setup + @professor = Professor.create(name: "Nicola") + + @connection = ActiveRecord::Base.connection + enable_extension!("postgres_fdw", @connection) + + foreign_db_config = ARTest.connection_config["arunit2"] + @connection.execute <<-SQL + CREATE SERVER foreign_server + FOREIGN DATA WRAPPER postgres_fdw + OPTIONS (dbname '#{foreign_db_config["database"]}') + SQL + + @connection.execute <<-SQL + CREATE USER MAPPING FOR CURRENT_USER + SERVER foreign_server + SQL + + @connection.execute <<-SQL + CREATE FOREIGN TABLE foreign_professors ( + id int, + name character varying NOT NULL + ) SERVER foreign_server OPTIONS ( + table_name 'professors' + ) + SQL + end + + def teardown + disable_extension!("postgres_fdw", @connection) + @connection.execute <<-SQL + DROP SERVER IF EXISTS foreign_server CASCADE + SQL + end + + def test_table_exists + table_name = ForeignProfessor.table_name + assert_not ActiveRecord::Base.connection.table_exists?(table_name) + end + + def test_foreign_tables_are_valid_data_sources + table_name = ForeignProfessor.table_name + assert @connection.data_source_exists?(table_name), "'#{table_name}' should be a data source" + end + + def test_foreign_tables + assert_equal ["foreign_professors"], @connection.foreign_tables + end + + def test_foreign_table_exists + assert @connection.foreign_table_exists?("foreign_professors") + assert @connection.foreign_table_exists?(:foreign_professors) + assert_not @connection.foreign_table_exists?("nonexistingtable") + assert_not @connection.foreign_table_exists?("'") + assert_not @connection.foreign_table_exists?(nil) + end + + def test_attribute_names + assert_equal ["id", "name"], ForeignProfessor.attribute_names + end + + def test_attributes + professor = ForeignProfessorWithPk.find(@professor.id) + assert_equal @professor.attributes, professor.attributes + end + + def test_does_not_have_a_primary_key + assert_nil ForeignProfessor.primary_key + end + + def test_insert_record + # Explicit `id` here to avoid complex configurations to implicitly work with remote table + ForeignProfessorWithPk.create!(id: 100, name: "Leonardo") + + professor = ForeignProfessorWithPk.last + assert_equal "Leonardo", professor.name + end + + def test_update_record + professor = ForeignProfessorWithPk.find(@professor.id) + professor.name = "Albert" + professor.save! + professor.reload + assert_equal "Albert", professor.name + end + + def test_delete_record + professor = ForeignProfessorWithPk.find(@professor.id) + assert_difference("ForeignProfessor.count", -1) { professor.destroy } + end + end +end diff --git a/activerecord/test/cases/adapters/postgresql/range_test.rb b/activerecord/test/cases/adapters/postgresql/range_test.rb index 813a8721a2..261c24634e 100644 --- a/activerecord/test/cases/adapters/postgresql/range_test.rb +++ b/activerecord/test/cases/adapters/postgresql/range_test.rb @@ -358,6 +358,18 @@ _SQL end end + def test_infinity_values + PostgresqlRange.create!(int4_range: 1..Float::INFINITY, + int8_range: -Float::INFINITY..0, + float_range: -Float::INFINITY..Float::INFINITY) + + record = PostgresqlRange.first + + assert_equal(1...Float::INFINITY, record.int4_range) + assert_equal(-Float::INFINITY...1, record.int8_range) + assert_equal(-Float::INFINITY...Float::INFINITY, record.float_range) + end + private def assert_equal_round_trip(range, attribute, value) round_trip(range, attribute, value) diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index cd5d6f17d8..20579c6476 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -500,6 +500,10 @@ module ActiveRecord end end + def test_deprecate_valid_alter_table_type + assert_deprecated { @conn.valid_alter_table_type?(:string) } + end + private def assert_logged(logs) diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb index 83974f327e..140d7cbcae 100644 --- a/activerecord/test/cases/ar_schema_test.rb +++ b/activerecord/test/cases/ar_schema_test.rb @@ -48,7 +48,7 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" } assert_nothing_raised { @connection.select_all "SELECT * FROM schema_migrations" } - assert_equal 7, ActiveRecord::Migrator::current_version + assert_equal 7, @connection.migration_context.current_version end def test_schema_define_w_table_name_prefix @@ -64,7 +64,7 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase t.column :flavor, :string end end - assert_equal 7, ActiveRecord::Migrator::current_version + assert_equal 7, @connection.migration_context.current_version ensure ActiveRecord::Base.table_name_prefix = old_table_name_prefix ActiveRecord::SchemaMigration.table_name = table_name diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 2649dc010f..9830917bc3 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -787,7 +787,7 @@ class EagerAssociationTest < ActiveRecord::TestCase Tagging.create!(taggable_type: "Post", taggable_id: post2.id, tag: tag) tag_with_includes = OrderedTag.includes(:tagged_posts).find(tag.id) - assert_equal(tag_with_includes.taggings.map(&:taggable).map(&:title), tag_with_includes.tagged_posts.map(&:title)) + assert_equal tag_with_includes.ordered_taggings.map(&:taggable).map(&:title), tag_with_includes.tagged_posts.map(&:title) end def test_eager_has_many_through_multiple_with_order 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 7c9c9e81ab..dabeeff1be 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -538,6 +538,16 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end end + def test_update_counter_caches_on_destroy_with_indestructible_through_record + post = posts(:welcome) + tag = post.indestructible_tags.create!(name: "doomed") + post.update_columns(indestructible_tags_count: post.indestructible_tags.count) + + assert_no_difference "post.reload.indestructible_tags_count" do + posts(:welcome).indestructible_tags.destroy(tag) + end + end + def test_replace_association assert_queries(4) { posts(:welcome);people(:david);people(:michael); posts(:welcome).people.reload } @@ -1308,6 +1318,70 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end end + def test_has_many_through_update_ids_with_conditions + author = Author.create!(name: "Bill") + category = categories(:general) + + author.update( + special_categories_with_condition_ids: [category.id], + nonspecial_categories_with_condition_ids: [category.id] + ) + + assert_equal [category.id], author.special_categories_with_condition_ids + assert_equal [category.id], author.nonspecial_categories_with_condition_ids + + author.update(nonspecial_categories_with_condition_ids: []) + author.reload + + assert_equal [category.id], author.special_categories_with_condition_ids + assert_equal [], author.nonspecial_categories_with_condition_ids + end + + def test_single_has_many_through_association_with_unpersisted_parent_instance + post_with_single_has_many_through = Class.new(Post) do + def self.name; "PostWithSingleHasManyThrough"; end + has_many :subscriptions, through: :author + end + post = post_with_single_has_many_through.new + + post.author = authors(:mary) + book1 = Book.create!(name: "essays on single has many through associations 1") + post.author.books << book1 + subscription1 = Subscription.first + book1.subscriptions << subscription1 + assert_equal [subscription1], post.subscriptions.to_a + + post.author = authors(:bob) + book2 = Book.create!(name: "essays on single has many through associations 2") + post.author.books << book2 + subscription2 = Subscription.second + book2.subscriptions << subscription2 + assert_equal [subscription2], post.subscriptions.to_a + end + + def test_nested_has_many_through_association_with_unpersisted_parent_instance + post_with_nested_has_many_through = Class.new(Post) do + def self.name; "PostWithNestedHasManyThrough"; end + has_many :books, through: :author + has_many :subscriptions, through: :books + end + post = post_with_nested_has_many_through.new + + post.author = authors(:mary) + book1 = Book.create!(name: "essays on nested has many through associations 1") + post.author.books << book1 + subscription1 = Subscription.first + book1.subscriptions << subscription1 + assert_equal [subscription1], post.subscriptions.to_a + + post.author = authors(:bob) + book2 = Book.create!(name: "essays on nested has many through associations 2") + post.author.books << book2 + subscription2 = Subscription.second + book2.subscriptions << subscription2 + assert_equal [subscription2], post.subscriptions.to_a + end + private def make_model(name) Class.new(ActiveRecord::Base) { define_singleton_method(:name) { name } } diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb index 1d37457464..8bbd4134fa 100644 --- a/activerecord/test/cases/associations/has_one_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb @@ -42,6 +42,18 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase assert_not_nil new_member.club end + def test_creating_association_builds_through_record + new_member = Member.create(name: "Chris") + new_club = new_member.association(:club).build + assert new_member.current_membership + assert_equal new_club, new_member.club + assert new_club.new_record? + assert new_member.current_membership.new_record? + assert new_member.save + assert new_club.persisted? + assert new_member.current_membership.persisted? + end + def test_creating_association_builds_through_record_for_new new_member = Member.new(name: "Jane") new_member.club = clubs(:moustache_club) diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 5eda39a0c7..e682da6fed 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -688,6 +688,11 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal [], Topic.includes(:replies).limit(1).where("0 = 1").pluck(:id) end + def test_pluck_with_includes_offset + assert_equal [5], Topic.includes(:replies).order(:id).offset(4).pluck(:id) + assert_equal [], Topic.includes(:replies).order(:id).offset(5).pluck(:id) + end + def test_pluck_not_auto_table_name_prefix_if_column_included Company.create!(name: "test", contracts: [Contract.new(developer_id: 7)]) ids = Company.includes(:contracts).pluck(:developer_id) diff --git a/activerecord/test/cases/connection_adapters/schema_cache_test.rb b/activerecord/test/cases/connection_adapters/schema_cache_test.rb index 006be9e65d..67496381d1 100644 --- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb +++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb @@ -22,8 +22,8 @@ module ActiveRecord new_cache = YAML.load(YAML.dump(@cache)) assert_no_queries do - assert_equal 11, new_cache.columns("posts").size - assert_equal 11, new_cache.columns_hash("posts").size + assert_equal 12, new_cache.columns("posts").size + assert_equal 12, new_cache.columns_hash("posts").size assert new_cache.data_sources("posts") assert_equal "id", new_cache.primary_keys("posts") end @@ -75,8 +75,8 @@ module ActiveRecord @cache = Marshal.load(Marshal.dump(@cache)) assert_no_queries do - assert_equal 11, @cache.columns("posts").size - assert_equal 11, @cache.columns_hash("posts").size + assert_equal 12, @cache.columns("posts").size + assert_equal 12, @cache.columns_hash("posts").size assert @cache.data_sources("posts") assert_equal "id", @cache.primary_keys("posts") end diff --git a/activerecord/test/cases/connection_adapters/type_lookup_test.rb b/activerecord/test/cases/connection_adapters/type_lookup_test.rb index 917a04ebc3..1c79d776f0 100644 --- a/activerecord/test/cases/connection_adapters/type_lookup_test.rb +++ b/activerecord/test/cases/connection_adapters/type_lookup_test.rb @@ -82,11 +82,11 @@ unless current_adapter?(:PostgreSQLAdapter) # PostgreSQL does not use type strin end def test_bigint_limit - cast_type = @connection.send(:type_map).lookup("bigint") + limit = @connection.send(:type_map).lookup("bigint").send(:_limit) if current_adapter?(:OracleAdapter) - assert_equal 19, cast_type.limit + assert_equal 19, limit else - assert_equal 8, cast_type.limit + assert_equal 8, limit end end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 8369a10b5a..4769ffd64d 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -270,25 +270,27 @@ class FinderTest < ActiveRecord::TestCase end def test_exists_with_includes_limit_and_empty_result - assert_equal false, Topic.includes(:replies).limit(0).exists? - assert_equal false, Topic.includes(:replies).limit(1).where("0 = 1").exists? + assert_no_queries { assert_equal false, Topic.includes(:replies).limit(0).exists? } + assert_queries(1) { assert_equal false, Topic.includes(:replies).limit(1).where("0 = 1").exists? } end def test_exists_with_distinct_association_includes_and_limit author = Author.first - assert_equal false, author.unique_categorized_posts.includes(:special_comments).limit(0).exists? - assert_equal true, author.unique_categorized_posts.includes(:special_comments).limit(1).exists? + unique_categorized_posts = author.unique_categorized_posts.includes(:special_comments) + assert_no_queries { assert_equal false, unique_categorized_posts.limit(0).exists? } + assert_queries(1) { assert_equal true, unique_categorized_posts.limit(1).exists? } end def test_exists_with_distinct_association_includes_limit_and_order author = Author.first - assert_equal false, author.unique_categorized_posts.includes(:special_comments).order("comments.tags_count DESC").limit(0).exists? - assert_equal true, author.unique_categorized_posts.includes(:special_comments).order("comments.tags_count DESC").limit(1).exists? + unique_categorized_posts = author.unique_categorized_posts.includes(:special_comments).order("comments.tags_count DESC") + assert_no_queries { assert_equal false, unique_categorized_posts.limit(0).exists? } + assert_queries(1) { assert_equal true, unique_categorized_posts.limit(1).exists? } end def test_exists_should_reference_correct_aliases_while_joining_tables_of_has_many_through_association - developer = developers(:david) - assert_not_predicate developer.ratings.includes(comment: :post).where(posts: { id: 1 }), :exists? + ratings = developers(:david).ratings.includes(comment: :post).where(posts: { id: 1 }) + assert_queries(1) { assert_not_predicate ratings.limit(1), :exists? } end def test_exists_with_empty_table_and_no_args_given @@ -677,12 +679,34 @@ class FinderTest < ActiveRecord::TestCase assert_equal comments.limit(2).to_a.last(2), comments.limit(2).last(2) assert_equal comments.limit(2).to_a.last(3), comments.limit(2).last(3) + assert_equal comments.offset(2).to_a.last, comments.offset(2).last + assert_equal comments.offset(2).to_a.last(2), comments.offset(2).last(2) + assert_equal comments.offset(2).to_a.last(3), comments.offset(2).last(3) + comments = comments.offset(1) assert_equal comments.limit(2).to_a.last, comments.limit(2).last assert_equal comments.limit(2).to_a.last(2), comments.limit(2).last(2) assert_equal comments.limit(2).to_a.last(3), comments.limit(2).last(3) end + def test_first_on_relation_with_limit_and_offset + post = posts("sti_comments") + + comments = post.comments.order(id: :asc) + assert_equal comments.limit(2).to_a.first, comments.limit(2).first + assert_equal comments.limit(2).to_a.first(2), comments.limit(2).first(2) + assert_equal comments.limit(2).to_a.first(3), comments.limit(2).first(3) + + assert_equal comments.offset(2).to_a.first, comments.offset(2).first + assert_equal comments.offset(2).to_a.first(2), comments.offset(2).first(2) + assert_equal comments.offset(2).to_a.first(3), comments.offset(2).first(3) + + comments = comments.offset(1) + assert_equal comments.limit(2).to_a.first, comments.limit(2).first + assert_equal comments.limit(2).to_a.first(2), comments.limit(2).first(2) + assert_equal comments.limit(2).to_a.first(3), comments.limit(2).first(3) + end + def test_take_and_first_and_last_with_integer_should_return_an_array assert_kind_of Array, Topic.take(5) assert_kind_of Array, Topic.first(5) @@ -1049,14 +1073,6 @@ class FinderTest < ActiveRecord::TestCase assert_raise(ArgumentError) { Topic.find_by_title_and_author_name("The First Topic") } end - def test_find_last_with_offset - devs = Developer.order("id") - - assert_equal devs[2], Developer.offset(2).first - assert_equal devs[-3], Developer.offset(2).last - assert_equal devs[-3], Developer.offset(2).order("id DESC").first - end - def test_find_by_nil_attribute topic = Topic.find_by_last_read nil assert_not_nil topic @@ -1301,12 +1317,12 @@ class FinderTest < ActiveRecord::TestCase test "#skip_query_cache! for #exists? with a limited eager load" do Topic.cache do - assert_queries(2) do + assert_queries(1) do Topic.eager_load(:replies).limit(1).exists? Topic.eager_load(:replies).limit(1).exists? end - assert_queries(4) do + assert_queries(2) do Topic.eager_load(:replies).limit(1).skip_query_cache!.exists? Topic.eager_load(:replies).limit(1).skip_query_cache!.exists? end diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 8e8a49af8e..d6b22e0a79 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -79,6 +79,146 @@ class FixturesTest < ActiveRecord::TestCase ActiveSupport::Notifications.unsubscribe(subscription) end end + + def test_bulk_insert_multiple_table_with_a_multi_statement_query + subscriber = InsertQuerySubscriber.new + subscription = ActiveSupport::Notifications.subscribe("sql.active_record", subscriber) + + create_fixtures("bulbs", "authors", "computers") + + expected_sql = <<-EOS.strip_heredoc.chop + INSERT INTO #{ActiveRecord::Base.connection.quote_table_name("bulbs")} .* + INSERT INTO #{ActiveRecord::Base.connection.quote_table_name("authors")} .* + INSERT INTO #{ActiveRecord::Base.connection.quote_table_name("computers")} .* + EOS + assert_equal 1, subscriber.events.size + assert_match(/#{expected_sql}/, subscriber.events.first) + ensure + ActiveSupport::Notifications.unsubscribe(subscription) + end + + def test_bulk_insert_with_a_multi_statement_query_raises_an_exception_when_any_insert_fails + require "models/aircraft" + + assert_equal false, Aircraft.columns_hash["wheels_count"].null + fixtures = { + "aircraft" => [ + { "name" => "working_aircrafts", "wheels_count" => 2 }, + { "name" => "broken_aircrafts", "wheels_count" => nil }, + ] + } + + assert_no_difference "Aircraft.count" do + assert_raises(ActiveRecord::NotNullViolation) do + ActiveRecord::Base.connection.insert_fixtures_set(fixtures) + end + end + end + end + + if current_adapter?(:Mysql2Adapter) + def test_insert_fixtures_set_raises_an_error_when_max_allowed_packet_is_smaller_than_fixtures_set_size + conn = ActiveRecord::Base.connection + mysql_margin = 2 + packet_size = 1024 + bytes_needed_to_have_a_1024_bytes_fixture = 855 + fixtures = { + "traffic_lights" => [ + { "location" => "US", "state" => ["NY"], "long_state" => ["a" * bytes_needed_to_have_a_1024_bytes_fixture] }, + ] + } + + conn.stubs(:max_allowed_packet).returns(packet_size - mysql_margin) + + error = assert_raises(ActiveRecord::ActiveRecordError) { conn.insert_fixtures_set(fixtures) } + assert_match(/Fixtures set is too large #{packet_size}\./, error.message) + end + + def test_insert_fixture_set_when_max_allowed_packet_is_bigger_than_fixtures_set_size + conn = ActiveRecord::Base.connection + packet_size = 1024 + fixtures = { + "traffic_lights" => [ + { "location" => "US", "state" => ["NY"], "long_state" => ["a" * 51] }, + ] + } + + conn.stubs(:max_allowed_packet).returns(packet_size) + + assert_difference "TrafficLight.count" do + conn.insert_fixtures_set(fixtures) + end + end + + def test_insert_fixtures_set_split_the_total_sql_into_two_chunks_smaller_than_max_allowed_packet + subscriber = InsertQuerySubscriber.new + subscription = ActiveSupport::Notifications.subscribe("sql.active_record", subscriber) + conn = ActiveRecord::Base.connection + packet_size = 1024 + fixtures = { + "traffic_lights" => [ + { "location" => "US", "state" => ["NY"], "long_state" => ["a" * 450] }, + ], + "comments" => [ + { "post_id" => 1, "body" => "a" * 450 }, + ] + } + + conn.stubs(:max_allowed_packet).returns(packet_size) + + conn.insert_fixtures_set(fixtures) + assert_equal 2, subscriber.events.size + assert_operator subscriber.events.first.bytesize, :<, packet_size + assert_operator subscriber.events.second.bytesize, :<, packet_size + ensure + ActiveSupport::Notifications.unsubscribe(subscription) + end + + def test_insert_fixtures_set_concat_total_sql_into_a_single_packet_smaller_than_max_allowed_packet + subscriber = InsertQuerySubscriber.new + subscription = ActiveSupport::Notifications.subscribe("sql.active_record", subscriber) + conn = ActiveRecord::Base.connection + packet_size = 1024 + fixtures = { + "traffic_lights" => [ + { "location" => "US", "state" => ["NY"], "long_state" => ["a" * 200] }, + ], + "comments" => [ + { "post_id" => 1, "body" => "a" * 200 }, + ] + } + + conn.stubs(:max_allowed_packet).returns(packet_size) + + assert_difference ["TrafficLight.count", "Comment.count"], +1 do + conn.insert_fixtures_set(fixtures) + end + assert_equal 1, subscriber.events.size + ensure + ActiveSupport::Notifications.unsubscribe(subscription) + end + end + + def test_no_auto_value_on_zero_is_disabled + skip unless current_adapter?(:Mysql2Adapter) + + begin + fixtures = [ + { "name" => "first", "wheels_count" => 2 }, + { "name" => "second", "wheels_count" => 3 } + ] + subscriber = InsertQuerySubscriber.new + subscription = ActiveSupport::Notifications.subscribe("sql.active_record", subscriber) + + assert_nothing_raised do + ActiveRecord::Base.connection.insert_fixtures(fixtures, "aircraft") + end + + expected_sql = "INSERT INTO `aircraft` (`id`, `name`, `wheels_count`) VALUES (DEFAULT, 'first', 2), (DEFAULT, 'second', 3)" + assert_equal expected_sql, subscriber.events.first + ensure + ActiveSupport::Notifications.unsubscribe(subscription) + end end def test_broken_yaml_exception diff --git a/activerecord/test/cases/migration/pending_migrations_test.rb b/activerecord/test/cases/migration/pending_migrations_test.rb index d0066f68be..dedb5ea502 100644 --- a/activerecord/test/cases/migration/pending_migrations_test.rb +++ b/activerecord/test/cases/migration/pending_migrations_test.rb @@ -4,37 +4,37 @@ require "cases/helper" module ActiveRecord class Migration - class PendingMigrationsTest < ActiveRecord::TestCase - def setup - super - @connection = Minitest::Mock.new - @app = Minitest::Mock.new - conn = @connection - @pending = Class.new(CheckPending) { - define_method(:connection) { conn } - }.new(@app) - @pending.instance_variable_set :@last_check, -1 # Force checking - end + if current_adapter?(:SQLite3Adapter) && !in_memory_db? + class PendingMigrationsTest < ActiveRecord::TestCase + setup do + file = ActiveRecord::Base.connection.raw_connection.filename + @conn = ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:", migrations_paths: MIGRATIONS_ROOT + "/valid" + source_db = SQLite3::Database.new file + dest_db = ActiveRecord::Base.connection.raw_connection + backup = SQLite3::Backup.new(dest_db, "main", source_db, "main") + backup.step(-1) + backup.finish + end - def teardown - assert @connection.verify - assert @app.verify - super - end + teardown do + @conn.release_connection if @conn + ActiveRecord::Base.establish_connection :arunit + end + + def test_errors_if_pending + ActiveRecord::Base.connection.drop_table "schema_migrations", if_exists: true - def test_errors_if_pending - ActiveRecord::Migrator.stub :needs_migration?, true do - assert_raise ActiveRecord::PendingMigrationError do - @pending.call(nil) + assert_raises ActiveRecord::PendingMigrationError do + CheckPending.new(Proc.new {}).call({}) end end - end - def test_checks_if_supported - @app.expect :call, nil, [:foo] + def test_checks_if_supported + ActiveRecord::SchemaMigration.create_table + migrator = Base.connection.migration_context + capture(:stdout) { migrator.migrate } - ActiveRecord::Migrator.stub :needs_migration?, false do - @pending.call(:foo) + assert_nil CheckPending.new(Proc.new {}).call({}) end end end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 3a6b5b2a6f..2ef055fb01 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -71,6 +71,16 @@ class MigrationTest < ActiveRecord::TestCase ActiveRecord::Migration.verbose = @verbose_was end + def test_migrator_migrations_path_is_deprecated + assert_deprecated do + ActiveRecord::Migrator.migrations_path = "/whatever" + end + ensure + assert_deprecated do + ActiveRecord::Migrator.migrations_path = "db/migrate" + end + end + def test_migration_version_matches_component_version assert_equal ActiveRecord::VERSION::STRING.to_f, ActiveRecord::Migration.current_version end @@ -78,20 +88,20 @@ class MigrationTest < ActiveRecord::TestCase def test_migrator_versions migrations_path = MIGRATIONS_ROOT + "/valid" old_path = ActiveRecord::Migrator.migrations_paths - ActiveRecord::Migrator.migrations_paths = migrations_path + migrator = ActiveRecord::MigrationContext.new(migrations_path) - ActiveRecord::Migrator.up(migrations_path) - assert_equal 3, ActiveRecord::Migrator.current_version - assert_equal false, ActiveRecord::Migrator.needs_migration? + migrator.up + assert_equal 3, migrator.current_version + assert_equal false, migrator.needs_migration? - ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid") - assert_equal 0, ActiveRecord::Migrator.current_version - assert_equal true, ActiveRecord::Migrator.needs_migration? + migrator.down + assert_equal 0, migrator.current_version + assert_equal true, migrator.needs_migration? ActiveRecord::SchemaMigration.create!(version: 3) - assert_equal true, ActiveRecord::Migrator.needs_migration? + assert_equal true, migrator.needs_migration? ensure - ActiveRecord::Migrator.migrations_paths = old_path + ActiveRecord::MigrationContext.new(old_path) end def test_migration_detection_without_schema_migration_table @@ -99,28 +109,31 @@ class MigrationTest < ActiveRecord::TestCase migrations_path = MIGRATIONS_ROOT + "/valid" old_path = ActiveRecord::Migrator.migrations_paths - ActiveRecord::Migrator.migrations_paths = migrations_path + migrator = ActiveRecord::MigrationContext.new(migrations_path) - assert_equal true, ActiveRecord::Migrator.needs_migration? + assert_equal true, migrator.needs_migration? ensure - ActiveRecord::Migrator.migrations_paths = old_path + ActiveRecord::MigrationContext.new(old_path) end def test_any_migrations old_path = ActiveRecord::Migrator.migrations_paths - ActiveRecord::Migrator.migrations_paths = MIGRATIONS_ROOT + "/valid" + migrator = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/valid") - assert ActiveRecord::Migrator.any_migrations? + assert migrator.any_migrations? - ActiveRecord::Migrator.migrations_paths = MIGRATIONS_ROOT + "/empty" + migrator_empty = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/empty") - assert_not ActiveRecord::Migrator.any_migrations? + assert_not migrator_empty.any_migrations? ensure - ActiveRecord::Migrator.migrations_paths = old_path + ActiveRecord::MigrationContext.new(old_path) end def test_migration_version - assert_nothing_raised { ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT + "/version_check", 20131219224947) } + migrator = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/version_check") + assert_equal 0, migrator.current_version + migrator.up(20131219224947) + assert_equal 20131219224947, migrator.current_version end def test_create_table_with_force_true_does_not_drop_nonexisting_table @@ -219,12 +232,13 @@ class MigrationTest < ActiveRecord::TestCase assert !Reminder.table_exists? name_filter = lambda { |migration| migration.name == "ValidPeopleHaveLastNames" } - ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid", &name_filter) + migrator = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/valid") + migrator.up(&name_filter) assert_column Person, :last_name assert_raise(ActiveRecord::StatementInvalid) { Reminder.first } - ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid", &name_filter) + migrator.down(&name_filter) assert_no_column Person, :last_name assert_raise(ActiveRecord::StatementInvalid) { Reminder.first } @@ -382,9 +396,9 @@ class MigrationTest < ActiveRecord::TestCase current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call migrations_path = MIGRATIONS_ROOT + "/valid" old_path = ActiveRecord::Migrator.migrations_paths - ActiveRecord::Migrator.migrations_paths = migrations_path + migrator = ActiveRecord::MigrationContext.new(migrations_path) - ActiveRecord::Migrator.up(migrations_path) + migrator.up assert_equal current_env, ActiveRecord::InternalMetadata[:environment] original_rails_env = ENV["RAILS_ENV"] @@ -395,13 +409,13 @@ class MigrationTest < ActiveRecord::TestCase refute_equal current_env, new_env sleep 1 # mysql by default does not store fractional seconds in the database - ActiveRecord::Migrator.up(migrations_path) + migrator.up assert_equal new_env, ActiveRecord::InternalMetadata[:environment] ensure - ActiveRecord::Migrator.migrations_paths = old_path + migrator = ActiveRecord::MigrationContext.new(old_path) ENV["RAILS_ENV"] = original_rails_env ENV["RACK_ENV"] = original_rack_env - ActiveRecord::Migrator.up(migrations_path) + migrator.up end def test_internal_metadata_stores_environment_when_other_data_exists @@ -411,14 +425,15 @@ class MigrationTest < ActiveRecord::TestCase current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call migrations_path = MIGRATIONS_ROOT + "/valid" old_path = ActiveRecord::Migrator.migrations_paths - ActiveRecord::Migrator.migrations_paths = migrations_path current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call - ActiveRecord::Migrator.up(migrations_path) + migrator = ActiveRecord::MigrationContext.new(migrations_path) + migrator.up assert_equal current_env, ActiveRecord::InternalMetadata[:environment] assert_equal "bar", ActiveRecord::InternalMetadata[:foo] ensure - ActiveRecord::Migrator.migrations_paths = old_path + migrator = ActiveRecord::MigrationContext.new(old_path) + migrator.up end def test_proper_table_name_on_migration diff --git a/activerecord/test/cases/migrator_test.rb b/activerecord/test/cases/migrator_test.rb index 1047ba1367..873455cf67 100644 --- a/activerecord/test/cases/migrator_test.rb +++ b/activerecord/test/cases/migrator_test.rb @@ -89,7 +89,7 @@ class MigratorTest < ActiveRecord::TestCase end def test_finds_migrations - migrations = ActiveRecord::Migrator.migrations(MIGRATIONS_ROOT + "/valid") + migrations = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/valid").migrations [[1, "ValidPeopleHaveLastNames"], [2, "WeNeedReminders"], [3, "InnocentJointable"]].each_with_index do |pair, i| assert_equal migrations[i].version, pair.first @@ -98,7 +98,8 @@ class MigratorTest < ActiveRecord::TestCase end def test_finds_migrations_in_subdirectories - migrations = ActiveRecord::Migrator.migrations(MIGRATIONS_ROOT + "/valid_with_subdirectories") + migrations = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/valid_with_subdirectories").migrations + [[1, "ValidPeopleHaveLastNames"], [2, "WeNeedReminders"], [3, "InnocentJointable"]].each_with_index do |pair, i| assert_equal migrations[i].version, pair.first @@ -108,7 +109,7 @@ class MigratorTest < ActiveRecord::TestCase def test_finds_migrations_from_two_directories directories = [MIGRATIONS_ROOT + "/valid_with_timestamps", MIGRATIONS_ROOT + "/to_copy_with_timestamps"] - migrations = ActiveRecord::Migrator.migrations directories + migrations = ActiveRecord::MigrationContext.new(directories).migrations [[20090101010101, "PeopleHaveHobbies"], [20090101010202, "PeopleHaveDescriptions"], @@ -121,14 +122,14 @@ class MigratorTest < ActiveRecord::TestCase end def test_finds_migrations_in_numbered_directory - migrations = ActiveRecord::Migrator.migrations [MIGRATIONS_ROOT + "/10_urban"] + migrations = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/10_urban").migrations assert_equal 9, migrations[0].version assert_equal "AddExpressions", migrations[0].name end def test_relative_migrations list = Dir.chdir(MIGRATIONS_ROOT) do - ActiveRecord::Migrator.migrations("valid") + ActiveRecord::MigrationContext.new("valid").migrations end migration_proxy = list.find { |item| @@ -157,7 +158,7 @@ class MigratorTest < ActiveRecord::TestCase ["up", "002", "We need reminders"], ["down", "003", "Innocent jointable"], ["up", "010", "********** NO FILE **********"], - ], ActiveRecord::Migrator.migrations_status(path) + ], ActiveRecord::MigrationContext.new(path).migrations_status end def test_migrations_status_in_subdirectories @@ -171,7 +172,7 @@ class MigratorTest < ActiveRecord::TestCase ["up", "002", "We need reminders"], ["down", "003", "Innocent jointable"], ["up", "010", "********** NO FILE **********"], - ], ActiveRecord::Migrator.migrations_status(path) + ], ActiveRecord::MigrationContext.new(path).migrations_status end def test_migrations_status_with_schema_define_in_subdirectories @@ -186,7 +187,7 @@ class MigratorTest < ActiveRecord::TestCase ["up", "001", "Valid people have last names"], ["up", "002", "We need reminders"], ["up", "003", "Innocent jointable"], - ], ActiveRecord::Migrator.migrations_status(path) + ], ActiveRecord::MigrationContext.new(path).migrations_status ensure ActiveRecord::Migrator.migrations_paths = prev_paths end @@ -204,7 +205,7 @@ class MigratorTest < ActiveRecord::TestCase ["down", "20100201010101", "Valid with timestamps we need reminders"], ["down", "20100301010101", "Valid with timestamps innocent jointable"], ["up", "20160528010101", "********** NO FILE **********"], - ], ActiveRecord::Migrator.migrations_status(paths) + ], ActiveRecord::MigrationContext.new(paths).migrations_status end def test_migrator_interleaved_migrations @@ -232,25 +233,28 @@ class MigratorTest < ActiveRecord::TestCase def test_up_calls_up migrations = [Sensor.new(nil, 0), Sensor.new(nil, 1), Sensor.new(nil, 2)] - ActiveRecord::Migrator.new(:up, migrations).migrate + migrator = ActiveRecord::Migrator.new(:up, migrations) + migrator.migrate assert migrations.all?(&:went_up) assert migrations.all? { |m| !m.went_down } - assert_equal 2, ActiveRecord::Migrator.current_version + assert_equal 2, migrator.current_version end def test_down_calls_down test_up_calls_up migrations = [Sensor.new(nil, 0), Sensor.new(nil, 1), Sensor.new(nil, 2)] - ActiveRecord::Migrator.new(:down, migrations).migrate + migrator = ActiveRecord::Migrator.new(:down, migrations) + migrator.migrate assert migrations.all? { |m| !m.went_up } assert migrations.all?(&:went_down) - assert_equal 0, ActiveRecord::Migrator.current_version + assert_equal 0, migrator.current_version end def test_current_version ActiveRecord::SchemaMigration.create!(version: "1000") - assert_equal 1000, ActiveRecord::Migrator.current_version + migrator = ActiveRecord::MigrationContext.new("db/migrate") + assert_equal 1000, migrator.current_version end def test_migrator_one_up @@ -289,33 +293,36 @@ class MigratorTest < ActiveRecord::TestCase def test_migrator_double_up calls, migrations = sensors(3) - assert_equal(0, ActiveRecord::Migrator.current_version) + migrator = ActiveRecord::Migrator.new(:up, migrations, 1) + assert_equal(0, migrator.current_version) - ActiveRecord::Migrator.new(:up, migrations, 1).migrate + migrator.migrate assert_equal [[:up, 1]], calls calls.clear - ActiveRecord::Migrator.new(:up, migrations, 1).migrate + migrator.migrate assert_equal [], calls end def test_migrator_double_down calls, migrations = sensors(3) + migrator = ActiveRecord::Migrator.new(:up, migrations, 1) - assert_equal(0, ActiveRecord::Migrator.current_version) + assert_equal 0, migrator.current_version - ActiveRecord::Migrator.new(:up, migrations, 1).run + migrator.run assert_equal [[:up, 1]], calls calls.clear - ActiveRecord::Migrator.new(:down, migrations, 1).run + migrator = ActiveRecord::Migrator.new(:down, migrations, 1) + migrator.run assert_equal [[:down, 1]], calls calls.clear - ActiveRecord::Migrator.new(:down, migrations, 1).run + migrator.run assert_equal [], calls - assert_equal(0, ActiveRecord::Migrator.current_version) + assert_equal 0, migrator.current_version end def test_migrator_verbosity @@ -361,78 +368,85 @@ class MigratorTest < ActiveRecord::TestCase def test_migrator_going_down_due_to_version_target calls, migrator = migrator_class(3) + migrator = migrator.new("valid") - migrator.up("valid", 1) + migrator.up(1) assert_equal [[:up, 1]], calls calls.clear - migrator.migrate("valid", 0) + migrator.migrate(0) assert_equal [[:down, 1]], calls calls.clear - migrator.migrate("valid") + migrator.migrate assert_equal [[:up, 1], [:up, 2], [:up, 3]], calls end def test_migrator_output_when_running_multiple_migrations _, migrator = migrator_class(3) + migrator = migrator.new("valid") - result = migrator.migrate("valid") + result = migrator.migrate assert_equal(3, result.count) # Nothing migrated from duplicate run - result = migrator.migrate("valid") + result = migrator.migrate assert_equal(0, result.count) - result = migrator.rollback("valid") + result = migrator.rollback assert_equal(1, result.count) end def test_migrator_output_when_running_single_migration _, migrator = migrator_class(1) - result = migrator.run(:up, "valid", 1) + migrator = migrator.new("valid") + + result = migrator.run(:up, 1) assert_equal(1, result.version) end def test_migrator_rollback _, migrator = migrator_class(3) + migrator = migrator.new("valid") - migrator.migrate("valid") - assert_equal(3, ActiveRecord::Migrator.current_version) + migrator.migrate + assert_equal(3, migrator.current_version) - migrator.rollback("valid") - assert_equal(2, ActiveRecord::Migrator.current_version) + migrator.rollback + assert_equal(2, migrator.current_version) - migrator.rollback("valid") - assert_equal(1, ActiveRecord::Migrator.current_version) + migrator.rollback + assert_equal(1, migrator.current_version) - migrator.rollback("valid") - assert_equal(0, ActiveRecord::Migrator.current_version) + migrator.rollback + assert_equal(0, migrator.current_version) - migrator.rollback("valid") - assert_equal(0, ActiveRecord::Migrator.current_version) + migrator.rollback + assert_equal(0, migrator.current_version) end def test_migrator_db_has_no_schema_migrations_table _, migrator = migrator_class(3) + migrator = migrator.new("valid") ActiveRecord::Base.connection.drop_table "schema_migrations", if_exists: true assert_not ActiveRecord::Base.connection.table_exists?("schema_migrations") - migrator.migrate("valid", 1) + migrator.migrate(1) assert ActiveRecord::Base.connection.table_exists?("schema_migrations") end def test_migrator_forward _, migrator = migrator_class(3) - migrator.migrate("/valid", 1) - assert_equal(1, ActiveRecord::Migrator.current_version) + migrator = migrator.new("/valid") + migrator.migrate(1) + assert_equal(1, migrator.current_version) - migrator.forward("/valid", 2) - assert_equal(3, ActiveRecord::Migrator.current_version) + migrator.forward(2) + assert_equal(3, migrator.current_version) - migrator.forward("/valid") - assert_equal(3, ActiveRecord::Migrator.current_version) + migrator.forward + assert_equal(3, migrator.current_version) end def test_only_loads_pending_migrations @@ -440,25 +454,27 @@ class MigratorTest < ActiveRecord::TestCase ActiveRecord::SchemaMigration.create!(version: "1") calls, migrator = migrator_class(3) - migrator.migrate("valid", nil) + migrator = migrator.new("valid") + migrator.migrate assert_equal [[:up, 2], [:up, 3]], calls end def test_get_all_versions _, migrator = migrator_class(3) + migrator = migrator.new("valid") - migrator.migrate("valid") - assert_equal([1, 2, 3], ActiveRecord::Migrator.get_all_versions) + migrator.migrate + assert_equal([1, 2, 3], migrator.get_all_versions) - migrator.rollback("valid") - assert_equal([1, 2], ActiveRecord::Migrator.get_all_versions) + migrator.rollback + assert_equal([1, 2], migrator.get_all_versions) - migrator.rollback("valid") - assert_equal([1], ActiveRecord::Migrator.get_all_versions) + migrator.rollback + assert_equal([1], migrator.get_all_versions) - migrator.rollback("valid") - assert_equal([], ActiveRecord::Migrator.get_all_versions) + migrator.rollback + assert_equal([], migrator.get_all_versions) end private @@ -483,11 +499,11 @@ class MigratorTest < ActiveRecord::TestCase def migrator_class(count) calls, migrations = sensors(count) - migrator = Class.new(ActiveRecord::Migrator).extend(Module.new { - define_method(:migrations) { |paths| + migrator = Class.new(ActiveRecord::MigrationContext) { + define_method(:migrations) { |*| migrations } - }) + } [calls, migrator] end end diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 0fa8ea212f..d242fff442 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -473,6 +473,22 @@ class PersistenceTest < ActiveRecord::TestCase assert_instance_of Reply, Reply.find(reply.id) end + def test_becomes_default_sti_subclass + original_type = Topic.columns_hash["type"].default + ActiveRecord::Base.connection.change_column_default :topics, :type, "Reply" + Topic.reset_column_information + + reply = topics(:second) + assert_instance_of Reply, reply + + topic = reply.becomes(Topic) + assert_instance_of Topic, topic + + ensure + ActiveRecord::Base.connection.change_column_default :topics, :type, original_type + Topic.reset_column_information + end + def test_update_after_create klass = Class.new(Topic) do def self.name; "Topic"; end @@ -599,9 +615,15 @@ class PersistenceTest < ActiveRecord::TestCase end def test_delete_new_record - client = Client.new + client = Client.new(name: "37signals") client.delete assert client.frozen? + + assert_not client.save + assert_raise(ActiveRecord::RecordNotSaved) { client.save! } + + assert client.frozen? + assert_raise(RuntimeError) { client.name = "something else" } end def test_delete_record_with_associations @@ -609,13 +631,24 @@ class PersistenceTest < ActiveRecord::TestCase client.delete assert client.frozen? assert_kind_of Firm, client.firm + + assert_not client.save + assert_raise(ActiveRecord::RecordNotSaved) { client.save! } + + assert client.frozen? assert_raise(RuntimeError) { client.name = "something else" } end def test_destroy_new_record - client = Client.new + client = Client.new(name: "37signals") client.destroy assert client.frozen? + + assert_not client.save + assert_raise(ActiveRecord::RecordNotSaved) { client.save! } + + assert client.frozen? + assert_raise(RuntimeError) { client.name = "something else" } end def test_destroy_record_with_associations @@ -623,6 +656,11 @@ class PersistenceTest < ActiveRecord::TestCase client.destroy assert client.frozen? assert_kind_of Firm, client.firm + + assert_not client.save + assert_raise(ActiveRecord::RecordNotSaved) { client.save! } + + assert client.frozen? assert_raise(RuntimeError) { client.name = "something else" } end diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index 44055e5ab6..e78c3cee8a 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -254,23 +254,34 @@ class ReflectionTest < ActiveRecord::TestCase end def test_scope_chain_does_not_interfere_with_hmt_with_polymorphic_case - @hotel = Hotel.create! - @department = @hotel.departments.create! - @department.chefs.create!(employable: CakeDesigner.create!) - @department.chefs.create!(employable: DrinkDesigner.create!) + hotel = Hotel.create! + department = hotel.departments.create! + department.chefs.create!(employable: CakeDesigner.create!) + department.chefs.create!(employable: DrinkDesigner.create!) - assert_equal 1, @hotel.cake_designers.size - assert_equal 1, @hotel.drink_designers.size - assert_equal 2, @hotel.chefs.size + assert_equal 1, hotel.cake_designers.size + assert_equal 1, hotel.cake_designers.count + assert_equal 1, hotel.drink_designers.size + assert_equal 1, hotel.drink_designers.count + assert_equal 2, hotel.chefs.size + assert_equal 2, hotel.chefs.count end def test_scope_chain_does_not_interfere_with_hmt_with_polymorphic_case_and_sti - @hotel = Hotel.create! - @hotel.mocktail_designers << MocktailDesigner.create! + hotel = Hotel.create! + hotel.mocktail_designers << MocktailDesigner.create! + + assert_equal 1, hotel.mocktail_designers.size + assert_equal 1, hotel.mocktail_designers.count + assert_equal 1, hotel.chef_lists.size + assert_equal 1, hotel.chef_lists.count + + hotel.mocktail_designers = [] - assert_equal 1, @hotel.mocktail_designers.size - assert_equal 1, @hotel.mocktail_designers.count - assert_equal 1, @hotel.chef_lists.size + assert_equal 0, hotel.mocktail_designers.size + assert_equal 0, hotel.mocktail_designers.count + assert_equal 0, hotel.chef_lists.size + assert_equal 0, hotel.chef_lists.count end def test_scope_chain_of_polymorphic_association_does_not_leak_into_other_hmt_associations diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb index b68b3723f6..f31df40c91 100644 --- a/activerecord/test/cases/relation/merging_test.rb +++ b/activerecord/test/cases/relation/merging_test.rb @@ -72,6 +72,12 @@ class RelationMergingTest < ActiveRecord::TestCase assert_equal 1, comments.count end + def test_relation_merging_with_left_outer_joins + comments = Comment.joins(:post).where(body: "Thank you for the welcome").merge(Post.left_outer_joins(:author).where(body: "Such a lovely day")) + + assert_equal 1, comments.count + end + def test_relation_merging_with_association assert_queries(2) do # one for loading post, and another one merged query post = Post.where(body: "Such a lovely day").first diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index b424ca91de..dbf3389774 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -24,10 +24,9 @@ module ActiveRecord def test_initialize_single_values relation = Relation.new(FakeKlass, :b, nil) - (Relation::SINGLE_VALUE_METHODS - [:create_with, :readonly]).each do |method| + (Relation::SINGLE_VALUE_METHODS - [:create_with]).each do |method| assert_nil relation.send("#{method}_value"), method.to_s end - assert_equal false, relation.readonly_value value = relation.create_with_value assert_equal({}, value) assert_predicate value, :frozen? diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb index fdfeabaa3b..6ff0f93cf3 100644 --- a/activerecord/test/cases/scoping/default_scoping_test.rb +++ b/activerecord/test/cases/scoping/default_scoping_test.rb @@ -224,6 +224,18 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal expected, received end + def test_unscope_left_outer_joins + expected = Developer.all.collect(&:name) + received = Developer.left_outer_joins(:projects).select(:id).unscope(:left_outer_joins, :select).collect(&:name) + assert_equal expected, received + end + + def test_unscope_left_joins + expected = Developer.all.collect(&:name) + received = Developer.left_joins(:projects).select(:id).unscope(:left_joins, :select).collect(&:name) + assert_equal expected, received + end + def test_unscope_includes expected = Developer.all.collect(&:name) received = Developer.includes(:projects).select(:id).unscope(:includes, :select).collect(&:name) diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb index c114842dec..21226352ff 100644 --- a/activerecord/test/cases/tasks/database_tasks_test.rb +++ b/activerecord/test/cases/tasks/database_tasks_test.rb @@ -28,10 +28,10 @@ module ActiveRecord class DatabaseTasksUtilsTask < ActiveRecord::TestCase def test_raises_an_error_when_called_with_protected_environment - ActiveRecord::Migrator.stubs(:current_version).returns(1) + ActiveRecord::MigrationContext.any_instance.stubs(:current_version).returns(1) protected_environments = ActiveRecord::Base.protected_environments - current_env = ActiveRecord::Migrator.current_environment + current_env = ActiveRecord::Base.connection.migration_context.current_environment assert_not_includes protected_environments, current_env # Assert no error ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! @@ -45,10 +45,10 @@ module ActiveRecord end def test_raises_an_error_when_called_with_protected_environment_which_name_is_a_symbol - ActiveRecord::Migrator.stubs(:current_version).returns(1) + ActiveRecord::MigrationContext.any_instance.stubs(:current_version).returns(1) protected_environments = ActiveRecord::Base.protected_environments - current_env = ActiveRecord::Migrator.current_environment + current_env = ActiveRecord::Base.connection.migration_context.current_environment assert_not_includes protected_environments, current_env # Assert no error ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! @@ -63,7 +63,7 @@ module ActiveRecord def test_raises_an_error_if_no_migrations_have_been_made ActiveRecord::InternalMetadata.stubs(:table_exists?).returns(false) - ActiveRecord::Migrator.stubs(:current_version).returns(1) + ActiveRecord::MigrationContext.any_instance.stubs(:current_version).returns(1) assert_raise(ActiveRecord::NoEnvironmentInSchemaError) do ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! @@ -347,50 +347,92 @@ module ActiveRecord end end - class DatabaseTasksMigrateTest < ActiveRecord::TestCase - self.use_transactional_tests = false + if current_adapter?(:SQLite3Adapter) && !in_memory_db? + class DatabaseTasksMigrateTest < ActiveRecord::TestCase + self.use_transactional_tests = false + + # Use a memory db here to avoid having to rollback at the end + setup do + migrations_path = MIGRATIONS_ROOT + "/valid" + file = ActiveRecord::Base.connection.raw_connection.filename + @conn = ActiveRecord::Base.establish_connection adapter: "sqlite3", + database: ":memory:", migrations_paths: migrations_path + source_db = SQLite3::Database.new file + dest_db = ActiveRecord::Base.connection.raw_connection + backup = SQLite3::Backup.new(dest_db, "main", source_db, "main") + backup.step(-1) + backup.finish + end - def setup - ActiveRecord::Tasks::DatabaseTasks.migrations_paths = "custom/path" - end + teardown do + @conn.release_connection if @conn + ActiveRecord::Base.establish_connection :arunit + end - def teardown - ActiveRecord::Tasks::DatabaseTasks.migrations_paths = nil - end + def test_migrate_set_and_unset_verbose_and_version_env_vars + verbose, version = ENV["VERBOSE"], ENV["VERSION"] + ENV["VERSION"] = "2" + ENV["VERBOSE"] = "false" - def test_migrate_receives_correct_env_vars - verbose, version = ENV["VERBOSE"], ENV["VERSION"] + # run down migration because it was already run on copied db + assert_empty capture_migration_output - ENV["VERBOSE"] = "false" - ENV["VERSION"] = "4" - ActiveRecord::Migrator.expects(:migrate).with("custom/path", 4) - ActiveRecord::Migration.expects(:verbose=).with(false) - ActiveRecord::Migration.expects(:verbose=).with(ActiveRecord::Migration.verbose) - ActiveRecord::Tasks::DatabaseTasks.migrate + ENV.delete("VERSION") + ENV.delete("VERBOSE") - ENV.delete("VERBOSE") - ENV.delete("VERSION") - ActiveRecord::Migrator.expects(:migrate).with("custom/path", nil) - ActiveRecord::Migration.expects(:verbose=).with(true) - ActiveRecord::Migration.expects(:verbose=).with(ActiveRecord::Migration.verbose) - ActiveRecord::Tasks::DatabaseTasks.migrate + # re-run up migration + assert_includes capture_migration_output, "migrating" + ensure + ENV["VERBOSE"], ENV["VERSION"] = verbose, version + end - ENV["VERBOSE"] = "" - ENV["VERSION"] = "" - ActiveRecord::Migrator.expects(:migrate).with("custom/path", nil) - ActiveRecord::Migration.expects(:verbose=).with(true) - ActiveRecord::Migration.expects(:verbose=).with(ActiveRecord::Migration.verbose) - ActiveRecord::Tasks::DatabaseTasks.migrate + def test_migrate_set_and_unset_empty_values_for_verbose_and_version_env_vars + verbose, version = ENV["VERBOSE"], ENV["VERSION"] - ENV["VERBOSE"] = "yes" - ENV["VERSION"] = "0" - ActiveRecord::Migrator.expects(:migrate).with("custom/path", 0) - ActiveRecord::Migration.expects(:verbose=).with(true) - ActiveRecord::Migration.expects(:verbose=).with(ActiveRecord::Migration.verbose) - ActiveRecord::Tasks::DatabaseTasks.migrate - ensure - ENV["VERBOSE"], ENV["VERSION"] = verbose, version + ENV["VERSION"] = "2" + ENV["VERBOSE"] = "false" + + # run down migration because it was already run on copied db + assert_empty capture_migration_output + + ENV["VERBOSE"] = "" + ENV["VERSION"] = "" + + # re-run up migration + assert_includes capture_migration_output, "migrating" + ensure + ENV["VERBOSE"], ENV["VERSION"] = verbose, version + end + + def test_migrate_set_and_unset_nonsense_values_for_verbose_and_version_env_vars + verbose, version = ENV["VERBOSE"], ENV["VERSION"] + + # run down migration because it was already run on copied db + ENV["VERSION"] = "2" + ENV["VERBOSE"] = "false" + + assert_empty capture_migration_output + + ENV["VERBOSE"] = "yes" + ENV["VERSION"] = "2" + + # run no migration because 2 was already run + assert_empty capture_migration_output + ensure + ENV["VERBOSE"], ENV["VERSION"] = verbose, version + end + + private + def capture_migration_output + capture(:stdout) do + ActiveRecord::Tasks::DatabaseTasks.migrate + end + end end + end + + class DatabaseTasksMigrateErrorTest < ActiveRecord::TestCase + self.use_transactional_tests = false def test_migrate_raise_error_on_invalid_version_format version = ENV["VERSION"] diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index cb8686f315..27da886e1c 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -88,6 +88,9 @@ class Author < ActiveRecord::Base has_many :special_categories, through: :special_categorizations, source: :category has_one :special_category, through: :special_categorizations, source: :category + has_many :special_categories_with_conditions, -> { where(categorizations: { special: true }) }, through: :categorizations, source: :category + has_many :nonspecial_categories_with_conditions, -> { where(categorizations: { special: false }) }, through: :categorizations, source: :category + has_many :categories_like_general, -> { where(name: "General") }, through: :categorizations, source: :category, class_name: "Category" has_many :categorized_posts, through: :categorizations, source: :post diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 780a2c17f5..b552f66787 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -106,6 +106,9 @@ class Post < ActiveRecord::Base end end + has_many :indestructible_taggings, as: :taggable, counter_cache: :indestructible_tags_count + has_many :indestructible_tags, through: :indestructible_taggings, source: :tag + has_many :taggings_with_delete_all, class_name: "Tagging", as: :taggable, dependent: :delete_all, counter_cache: :taggings_with_delete_all_count has_many :taggings_with_destroy, class_name: "Tagging", as: :taggable, dependent: :destroy, counter_cache: :taggings_with_destroy_count diff --git a/activerecord/test/models/tag.rb b/activerecord/test/models/tag.rb index bc13c3a42d..c1a8890a8a 100644 --- a/activerecord/test/models/tag.rb +++ b/activerecord/test/models/tag.rb @@ -11,6 +11,6 @@ end class OrderedTag < Tag self.table_name = "tags" - has_many :taggings, -> { order("taggings.id DESC") }, foreign_key: "tag_id" - has_many :tagged_posts, through: :taggings, source: "taggable", source_type: "Post" + has_many :ordered_taggings, -> { order("taggings.id DESC") }, foreign_key: "tag_id", class_name: "Tagging" + has_many :tagged_posts, through: :ordered_taggings, source: "taggable", source_type: "Post" end diff --git a/activerecord/test/models/tagging.rb b/activerecord/test/models/tagging.rb index 861fde633f..6d4230f6f4 100644 --- a/activerecord/test/models/tagging.rb +++ b/activerecord/test/models/tagging.rb @@ -14,3 +14,7 @@ class Tagging < ActiveRecord::Base belongs_to :taggable, polymorphic: true, counter_cache: :tags_count has_many :things, through: :taggable end + +class IndestructibleTagging < Tagging + before_destroy { throw :abort } +end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 3205c4c20a..7d008eecd5 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -690,6 +690,7 @@ ActiveRecord::Schema.define do t.integer :taggings_with_delete_all_count, default: 0 t.integer :taggings_with_destroy_count, default: 0 t.integer :tags_count, default: 0 + t.integer :indestructible_tags_count, default: 0 t.integer :tags_with_destroy_count, default: 0 t.integer :tags_with_nullify_count, default: 0 end @@ -847,6 +848,7 @@ ActiveRecord::Schema.define do t.column :taggable_type, :string t.column :taggable_id, :integer t.string :comment + t.string :type end create_table :tasks, force: true do |t| |