diff --git a/RELEASING_RAILS.md b/RELEASING_RAILS.md index cd850e6ddf..e5e3c9f91e 100644 --- a/RELEASING_RAILS.md +++ b/RELEASING_RAILS.md @@ -21,7 +21,7 @@ https://buildkite.com/rails/rails Sam Ruby keeps a [test suite](https://github.com/rubys/awdwr) that makes sure the code samples in his book -([Agile Web Development with Rails](https://pragprog.com/book/rails51/agile-web-development-with-rails-51)) +([Agile Web Development with Rails](https://pragprog.com/titles/rails6)) all work. These are valuable system tests for Rails. You can check the status of these tests here: diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 2b6582df78..3a4b52cc58 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,7 @@ +* Allow `assert_recognizes` routing assertions to work on mounted root routes. + + *Gannon McGibbon* + * Change default redirection status code for non-GET/HEAD requests to 308 Permanent Redirect for `ActionDispatch::SSL`. *Alan Tan*, *Oz Ben-David* diff --git a/actionpack/lib/action_dispatch/journey/router.rb b/actionpack/lib/action_dispatch/journey/router.rb index 5679dde71d..b6b71720db 100644 --- a/actionpack/lib/action_dispatch/journey/router.rb +++ b/actionpack/lib/action_dispatch/journey/router.rb @@ -66,7 +66,8 @@ module ActionDispatch find_routes(rails_req).each do |match, parameters, route| unless route.path.anchored rails_req.script_name = match.to_s - rails_req.path_info = match.post_match.sub(/^([^\/])/, '/\1') + rails_req.path_info = match.post_match + rails_req.path_info = "/" + rails_req.path_info unless rails_req.path_info.start_with? "/" end parameters = route.defaults.merge parameters diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb index 071097af1a..35288de822 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb @@ -199,7 +199,8 @@ module ActionDispatch method = :get end - request = ActionController::TestRequest.create @controller.class + controller = @controller if defined?(@controller) + request = ActionController::TestRequest.create controller&.class if %r{://}.match?(path) fail_on(URI::InvalidURIError, msg) do diff --git a/actionpack/test/dispatch/routing_assertions_test.rb b/actionpack/test/dispatch/routing_assertions_test.rb index 009b6d9bc3..760392d9df 100644 --- a/actionpack/test/dispatch/routing_assertions_test.rb +++ b/actionpack/test/dispatch/routing_assertions_test.rb @@ -14,11 +14,22 @@ class QueryBooksController < BooksController; end class RoutingAssertionsTest < ActionController::TestCase def setup + root_engine = Class.new(Rails::Engine) do + def self.name + "root_engine" + end + end + + root_engine.routes.draw do + root to: "books#index" + end + engine = Class.new(Rails::Engine) do def self.name "blog_engine" end end + engine.routes.draw do resources :books @@ -53,6 +64,8 @@ class RoutingAssertionsTest < ActionController::TestCase mount engine => "/shelf" + mount root_engine => "/" + get "/shelf/foo", controller: "query_articles", action: "index" end end @@ -118,6 +131,10 @@ class RoutingAssertionsTest < ActionController::TestCase assert_recognizes({ controller: "books", action: "show", id: "1" }, "/shelf/books/1") end + def test_assert_recognizes_with_engine_at_root + assert_recognizes({ controller: "books", action: "index" }, "/") + end + def test_assert_recognizes_with_engine_and_extras assert_recognizes({ controller: "books", action: "index", page: "1" }, "/shelf/books", page: "1") end diff --git a/actionview/lib/action_view/base.rb b/actionview/lib/action_view/base.rb index 3840ac0caf..ab9e217648 100644 --- a/actionview/lib/action_view/base.rb +++ b/actionview/lib/action_view/base.rb @@ -190,6 +190,14 @@ module ActionView #:nodoc: # correctly. define_method(:compiled_method_container) { subclass } define_singleton_method(:compiled_method_container) { subclass } + + def self.name + superclass.name + end + + def inspect + "#<#{self.class.name}:#{'%#016x' % (object_id << 1)}>" + end } end diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb index 91794fe4e4..cbf64867aa 100644 --- a/actionview/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb @@ -86,11 +86,11 @@ module ActionView def javascript_include_tag(*sources) options = sources.extract_options!.stringify_keys path_options = options.extract!("protocol", "extname", "host", "skip_pipeline").symbolize_keys - early_hints_links = [] + preload_links = [] sources_tags = sources.uniq.map { |source| href = path_to_javascript(source, path_options) - early_hints_links << "<#{href}>; rel=preload; as=script" + preload_links << "<#{href}>; rel=preload; as=script" tag_options = { "src" => href }.merge!(options) @@ -100,7 +100,7 @@ module ActionView content_tag("script", "", tag_options) }.join("\n").html_safe - request.send_early_hints("Link" => early_hints_links.join("\n")) if respond_to?(:request) && request + send_preload_links_header(preload_links) sources_tags end @@ -136,11 +136,11 @@ module ActionView def stylesheet_link_tag(*sources) options = sources.extract_options!.stringify_keys path_options = options.extract!("protocol", "host", "skip_pipeline").symbolize_keys - early_hints_links = [] + preload_links = [] sources_tags = sources.uniq.map { |source| href = path_to_stylesheet(source, path_options) - early_hints_links << "<#{href}>; rel=preload; as=style" + preload_links << "<#{href}>; rel=preload; as=style" tag_options = { "rel" => "stylesheet", "media" => "screen", @@ -149,7 +149,7 @@ module ActionView tag(:link, tag_options) }.join("\n").html_safe - request.send_early_hints("Link" => early_hints_links.join("\n")) if respond_to?(:request) && request + send_preload_links_header(preload_links) sources_tags end @@ -281,12 +281,12 @@ module ActionView crossorigin: crossorigin }.merge!(options.symbolize_keys)) - early_hints_link = "<#{href}>; rel=preload; as=#{as_type}" - early_hints_link += "; type=#{mime_type}" if mime_type - early_hints_link += "; crossorigin=#{crossorigin}" if crossorigin - early_hints_link += "; nopush" if nopush + preload_link = "<#{href}>; rel=preload; as=#{as_type}" + preload_link += "; type=#{mime_type}" if mime_type + preload_link += "; crossorigin=#{crossorigin}" if crossorigin + preload_link += "; nopush" if nopush - request.send_early_hints("Link" => early_hints_link) if respond_to?(:request) && request + send_preload_links_header([preload_link]) link_tag end @@ -482,6 +482,16 @@ module ActionView type end end + + def send_preload_links_header(preload_links) + if respond_to?(:request) && request + request.send_early_hints("Link" => preload_links.join("\n")) + end + + if respond_to?(:response) && response + response.headers["Link"] = [response.headers["Link"].presence, *preload_links].compact.join(",") + end + end end end end diff --git a/actionview/test/abstract_unit.rb b/actionview/test/abstract_unit.rb index ddd34da7bd..57296e090e 100644 --- a/actionview/test/abstract_unit.rb +++ b/actionview/test/abstract_unit.rb @@ -84,7 +84,7 @@ class RoutedRackApp end class BasicController - attr_accessor :request + attr_accessor :request, :response def config @config ||= ActiveSupport::InheritableOptions.new(ActionController::Base.config).tap do |config| @@ -151,7 +151,7 @@ module ActionController define_method(:setup) do super() @routes = routes - @controller.singleton_class.include @routes.url_helpers + @controller.singleton_class.include @routes.url_helpers if @controller end } routes diff --git a/actionview/test/template/asset_tag_helper_test.rb b/actionview/test/template/asset_tag_helper_test.rb index efef74ca00..a59929a2ea 100644 --- a/actionview/test/template/asset_tag_helper_test.rb +++ b/actionview/test/template/asset_tag_helper_test.rb @@ -9,23 +9,33 @@ ActionView::Template::Types.delegate_to Mime class AssetTagHelperTest < ActionView::TestCase tests ActionView::Helpers::AssetTagHelper - attr_reader :request + attr_reader :request, :response + + class FakeRequest + attr_accessor :script_name + def protocol() "http://" end + def ssl?() false end + def host_with_port() "localhost" end + def base_url() "http://www.example.com" end + def send_early_hints(links) end + end + + class FakeResponse + def headers + @headers ||= {} + end + end def setup super @controller = BasicController.new - @request = Class.new do - attr_accessor :script_name - def protocol() "http://" end - def ssl?() false end - def host_with_port() "localhost" end - def base_url() "http://www.example.com" end - def send_early_hints(links) end - end.new - + @request = FakeRequest.new @controller.request = @request + + @response = FakeResponse.new + @controller.response = @response end def url_for(*args) @@ -499,6 +509,14 @@ class AssetTagHelperTest < ActionView::TestCase assert_dom_equal %(), javascript_include_tag("foo.js") end + + def test_should_set_preload_links + stylesheet_link_tag("http://example.com/style.css") + javascript_include_tag("http://example.com/all.js") + expected = "; rel=preload; as=style,; rel=preload; as=script" + assert_equal expected, @response.headers["Link"] + end + def test_image_path ImagePathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } end diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb index 82dd56b99f..541b0b13b7 100644 --- a/actionview/test/template/render_test.rb +++ b/actionview/test/template/render_test.rb @@ -327,6 +327,11 @@ module RenderTestCases assert_equal File.expand_path("#{FIXTURE_LOAD_PATH}/test/_raise.html.erb"), e.file_name end + def test_undefined_method_error_references_named_class + e = assert_raises(ActionView::Template::Error) { @view.render(inline: "<%= undefined %>") } + assert_match(/`undefined' for #/, e.message) + end + def test_render_object assert_equal "Hello: david", @view.render(partial: "test/customer", object: Customer.new("david")) assert_equal "FalseClass", @view.render(partial: "test/klass", object: false) diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 4fe24744db..337fc8a350 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -4,6 +4,26 @@ *fatkodima* +* Respect the `select` values for eager loading. + + ```ruby + post = Post.select("UPPER(title) AS title").first + post.title # => "WELCOME TO THE WEBLOG" + post.body # => ActiveModel::MissingAttributeError + + # Rails 6.0 (ignore the `select` values) + post = Post.select("UPPER(title) AS title").eager_load(:comments).first + post.title # => "Welcome to the weblog" + post.body # => "Such a lovely day" + + # Rails 6.1 (respect the `select` values) + post = Post.select("UPPER(title) AS title").eager_load(:comments).first + post.title # => "WELCOME TO THE WEBLOG" + post.body # => ActiveModel::MissingAttributeError + ``` + + *Ryuta Kamizono* + * Allow attribute's default to be configured but keeping its own type. ```ruby diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index e9a7623f40..4d524623a3 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -36,7 +36,6 @@ require "active_record/errors" module ActiveRecord extend ActiveSupport::Autoload - autoload :AdvisoryLockBase autoload :Base autoload :Callbacks autoload :Core diff --git a/activerecord/lib/active_record/advisory_lock_base.rb b/activerecord/lib/active_record/advisory_lock_base.rb deleted file mode 100644 index 7110a1f132..0000000000 --- a/activerecord/lib/active_record/advisory_lock_base.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -module ActiveRecord - # This class is used to create a connection that we can use for advisory - # locks. This will take out a "global" lock that can't be accidentally - # removed if a new connection is established during a migration. - class AdvisoryLockBase < ActiveRecord::Base # :nodoc: - self.abstract_class = true - - self.connection_specification_name = "AdvisoryLockBase" - - class << self - def _internal? - true - end - end - end -end diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index b0eaec66a1..8f2c6f269c 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -52,7 +52,7 @@ module ActiveRecord attr_reader :value_transformation def join(table, constraint) - table.create_join(table, table.create_on(constraint), Arel::Nodes::LeadingJoin) + Arel::Nodes::LeadingJoin.new(table, Arel::Nodes::On.new(constraint)) end def last_chain_scope(scope, reflection, owner) diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index eb3fb0b74f..0c8978be1f 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -34,7 +34,7 @@ module ActiveRecord Table = Struct.new(:node, :columns) do # :nodoc: def column_aliases t = node.table - columns.map { |column| t[column.name].as Arel.sql column.alias } + columns.map { |column| t[column.name].as(column.alias) } end end Column = Struct.new(:name, :alias) @@ -80,6 +80,7 @@ module ActiveRecord def join_constraints(joins_to_add, alias_tracker) @alias_tracker = alias_tracker + @joined_tables = {} joins = make_join_constraints(join_root, join_type) @@ -105,13 +106,20 @@ module ActiveRecord parents = model_cache[join_root] column_aliases = aliases.column_aliases(join_root) - column_names = explicit_selections(column_aliases, result_set) + column_names = [] + + result_set.columns.each do |name| + column_names << name unless /\At\d+_r\d+\z/.match?(name) + end if column_names.empty? column_types = {} else column_types = result_set.column_types - column_types = column_types.slice(*column_names) unless column_types.empty? + unless column_types.empty? + attribute_types = join_root.attribute_types + column_types = column_types.slice(*column_names).delete_if { |k, _| attribute_types.key?(k) } + end column_aliases += column_names.map! { |name| Aliases::Column.new(name, name) } end @@ -134,6 +142,7 @@ module ActiveRecord end def apply_column_aliases(relation) + @join_root_alias = relation.select_values.empty? relation._select!(-> { aliases.columns }) end @@ -145,18 +154,18 @@ module ActiveRecord attr_reader :join_root, :join_type private - attr_reader :alias_tracker - - def explicit_selections(root_column_aliases, result_set) - root_names = root_column_aliases.map(&:name).to_set - result_set.columns.each_with_object([]) do |name, result| - result << name unless /\At\d+_r\d+\z/.match?(name) || root_names.include?(name) - end - end + attr_reader :alias_tracker, :join_root_alias def aliases @aliases ||= Aliases.new join_root.each_with_index.map { |join_part, i| - columns = join_part.column_names.each_with_index.map { |column_name, j| + column_names = if join_part == join_root && !join_root_alias + primary_key = join_root.primary_key + primary_key ? [primary_key] : [] + else + join_part.column_names + end + + columns = column_names.each_with_index.map { |column_name, j| Aliases::Column.new column_name, "t#{i}_r#{j}" } Aliases::Table.new(join_part, columns) @@ -173,15 +182,22 @@ module ActiveRecord foreign_table = parent.table foreign_klass = parent.base_klass child.join_constraints(foreign_table, foreign_klass, join_type, alias_tracker) do |reflection| - alias_tracker.aliased_table_for(reflection.klass.arel_table) do - table_alias_for(reflection, parent, reflection != child.reflection) - end - end.concat child.children.flat_map { |c| make_constraints(child, c, join_type) } - end + table, terminated = @joined_tables[reflection] + root = reflection == child.reflection - def table_alias_for(reflection, parent, join) - name = reflection.alias_candidate(parent.table_name) - join ? "#{name}_join" : name + if table && (!root || !terminated) + @joined_tables[reflection] = [table, root] if root + next table, true + end + + table = alias_tracker.aliased_table_for(reflection.klass.arel_table) do + name = reflection.alias_candidate(parent.table_name) + root ? name : "#{name}_join" + end + + @joined_tables[reflection] ||= [table, root] if join_type == Arel::Nodes::OuterJoin + table + end.concat child.children.flat_map { |c| make_constraints(child, c, join_type) } end def walk(left, right, join_type) diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb index 1f1e1ba6b5..1bc9802e6a 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -21,15 +21,25 @@ module ActiveRecord super && reflection == other.reflection end - def join_constraints(foreign_table, foreign_klass, join_type, alias_tracker, &block) + def join_constraints(foreign_table, foreign_klass, join_type, alias_tracker) joins = [] - tables = reflection.chain.map(&block) - @table = tables.first + chain = [] + + reflection.chain.each do |reflection| + table, terminated = yield reflection + @table ||= table + + if terminated + foreign_table, foreign_klass = table, reflection.klass + break + end + + chain << [reflection, table] + end # The chain starts with the target table, but we want to end with it here (makes # more sense in this context), so we reverse - reflection.chain.reverse_each.with_index(1) do |reflection, i| - table = tables[-i] + chain.reverse_each do |reflection, table| klass = reflection.klass join_scope = reflection.join_scope(table, foreign_table, foreign_klass) @@ -50,7 +60,7 @@ module ActiveRecord end end - joins << table.create_join(table, table.create_on(nodes), join_type) + joins << join_type.new(table, Arel::Nodes::On.new(nodes)) if others && !others.empty? joins.concat arel.join_sources @@ -79,7 +89,7 @@ module ActiveRecord private def append_constraints(join, constraints) if join.is_a?(Arel::Nodes::StringJoin) - join_string = table.create_and(constraints.unshift(join.left)) + join_string = Arel::Nodes::And.new(constraints.unshift join.left) join.left = Arel.sql(base_klass.connection.visitor.compile(join_string)) else right = join.right diff --git a/activerecord/lib/active_record/associations/join_dependency/join_part.rb b/activerecord/lib/active_record/associations/join_dependency/join_part.rb index cf551f78b0..3ffa079c61 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb @@ -17,7 +17,7 @@ module ActiveRecord # association. attr_reader :base_klass, :children - delegate :table_name, :column_names, :primary_key, to: :base_klass + delegate :table_name, :column_names, :primary_key, :attribute_types, to: :base_klass def initialize(base_klass, children) @base_klass = base_klass diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 304d7d1a3a..de7288983a 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -96,6 +96,10 @@ module ActiveRecord end end + def initialize(associate_by_default: true) + @associate_by_default = associate_by_default + end + private # Loads all the given data into +records+ for the +association+. def preloaders_on(association, records, scope, polymorphic_parent = false) @@ -144,7 +148,7 @@ module ActiveRecord def preloaders_for_reflection(reflection, records, scope) records.group_by { |record| record.association(reflection.name).klass }.map do |rhs_klass, rs| - preloader_for(reflection, rs).new(rhs_klass, rs, reflection, scope).run + preloader_for(reflection, rs).new(rhs_klass, rs, reflection, scope, @associate_by_default).run end end @@ -159,7 +163,7 @@ module ActiveRecord end class AlreadyLoaded # :nodoc: - def initialize(klass, owners, reflection, preload_scope) + def initialize(klass, owners, reflection, preload_scope, associate_by_default = true) @owners = owners @reflection = reflection end diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 5ea9f908ef..8ce6a8eecf 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -4,25 +4,22 @@ module ActiveRecord module Associations class Preloader class Association #:nodoc: - def initialize(klass, owners, reflection, preload_scope) + def initialize(klass, owners, reflection, preload_scope, associate_by_default = true) @klass = klass @owners = owners.uniq(&:__id__) @reflection = reflection @preload_scope = preload_scope + @associate = associate_by_default || !preload_scope || preload_scope.empty_scope? @model = owners.first && owners.first.class end def run - if !preload_scope || preload_scope.empty_scope? - owners.each do |owner| - associate_records_to_owner(owner, records_by_owner[owner] || []) - end - else - # Custom preload scope is used and - # the association cannot be marked as loaded - # Loading into a Hash instead - records_by_owner - end + records = records_by_owner + + owners.each do |owner| + associate_records_to_owner(owner, records[owner] || []) + end if @associate + self end diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index 13aae3237b..b39a235a19 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -4,7 +4,7 @@ module ActiveRecord module Associations class Preloader class ThroughAssociation < Association # :nodoc: - PRELOADER = ActiveRecord::Associations::Preloader.new + PRELOADER = ActiveRecord::Associations::Preloader.new(associate_by_default: false) def initialize(*) super diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index f86b7dadfc..70e48de940 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -1027,11 +1027,11 @@ module ActiveRecord end def connection_pool_list - owner_to_pool_manager.values.compact.flat_map { |m| m.pool_configs.map(&:pool) } + owner_to_pool_manager.values.flat_map { |m| m.pool_configs.map(&:pool) } end alias :connection_pools :connection_pool_list - def establish_connection(config, pool_key = Base.default_pool_key, owner_name = Base.name) + def establish_connection(config, owner_name: Base.name, shard: Base.default_shard) owner_name = config.to_s if config.is_a?(Symbol) pool_config = resolve_pool_config(config, owner_name) @@ -1040,7 +1040,7 @@ module ActiveRecord # Protects the connection named `ActiveRecord::Base` from being removed # if the user calls `establish_connection :primary`. if owner_to_pool_manager.key?(pool_config.connection_specification_name) - remove_connection_pool(pool_config.connection_specification_name, pool_key) + remove_connection_pool(pool_config.connection_specification_name, shard: shard) end message_bus = ActiveSupport::Notifications.instrumenter @@ -1052,7 +1052,7 @@ module ActiveRecord owner_to_pool_manager[pool_config.connection_specification_name] ||= PoolManager.new pool_manager = get_pool_manager(pool_config.connection_specification_name) - pool_manager.set_pool_config(pool_key, pool_config) + pool_manager.set_pool_config(shard, pool_config) message_bus.instrument("!connection.active_record", payload) do pool_config.pool @@ -1094,12 +1094,12 @@ module ActiveRecord # active or defined connection: if it is the latter, it will be # opened and set as the active connection for the class it was defined # for (not necessarily the current class). - def retrieve_connection(spec_name, pool_key = ActiveRecord::Base.default_pool_key) # :nodoc: - pool = retrieve_connection_pool(spec_name, pool_key) + def retrieve_connection(spec_name, shard: ActiveRecord::Base.default_shard) # :nodoc: + pool = retrieve_connection_pool(spec_name, shard: shard) unless pool - if pool_key != ActiveRecord::Base.default_pool_key - message = "No connection pool for '#{spec_name}' found for the '#{pool_key}' shard." + if shard != ActiveRecord::Base.default_shard + message = "No connection pool for '#{spec_name}' found for the '#{shard}' shard." elsif ActiveRecord::Base.connection_handler != ActiveRecord::Base.default_connection_handler message = "No connection pool for '#{spec_name}' found for the '#{ActiveRecord::Base.current_role}' role." else @@ -1114,8 +1114,8 @@ module ActiveRecord # Returns true if a connection that's accessible to this class has # already been opened. - def connected?(spec_name, pool_key = ActiveRecord::Base.default_pool_key) - pool = retrieve_connection_pool(spec_name, pool_key) + def connected?(spec_name, shard: ActiveRecord::Base.default_shard) + pool = retrieve_connection_pool(spec_name, shard: shard) pool && pool.connected? end @@ -1123,14 +1123,14 @@ module ActiveRecord # connection and the defined connection (if they exist). The result # can be used as an argument for #establish_connection, for easily # re-establishing the connection. - def remove_connection(owner, pool_key = ActiveRecord::Base.default_pool_key) - remove_connection_pool(owner, pool_key)&.configuration_hash + def remove_connection(owner, shard: ActiveRecord::Base.default_shard) + remove_connection_pool(owner, shard: shard)&.configuration_hash end deprecate remove_connection: "Use #remove_connection_pool, which now returns a DatabaseConfig object instead of a Hash" - def remove_connection_pool(owner, pool_key = ActiveRecord::Base.default_pool_key) + def remove_connection_pool(owner, shard: ActiveRecord::Base.default_shard) if pool_manager = get_pool_manager(owner) - pool_config = pool_manager.remove_pool_config(pool_key) + pool_config = pool_manager.remove_pool_config(shard) if pool_config pool_config.disconnect! @@ -1142,8 +1142,8 @@ module ActiveRecord # Retrieving the connection pool happens a lot, so we cache it in @owner_to_pool_manager. # This makes retrieving the connection pool O(1) once the process is warm. # When a connection is established or removed, we invalidate the cache. - def retrieve_connection_pool(owner, pool_key = ActiveRecord::Base.default_pool_key) - pool_config = get_pool_manager(owner)&.get_pool_config(pool_key) + def retrieve_connection_pool(owner, shard: ActiveRecord::Base.default_shard) + pool_config = get_pool_manager(owner)&.get_pool_config(shard) pool_config&.pool end diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index 19094b6ccd..8ddf8dc183 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -49,7 +49,7 @@ module ActiveRecord def establish_connection(config_or_env = nil) config_or_env ||= DEFAULT_ENV.call.to_sym db_config, owner_name = resolve_config_for_connection(config_or_env) - connection_handler.establish_connection(db_config, current_pool_key, owner_name) + connection_handler.establish_connection(db_config, owner_name: owner_name, shard: current_shard) end # Connects a model to the databases specified. The +database+ keyword @@ -89,15 +89,15 @@ module ActiveRecord db_config, owner_name = resolve_config_for_connection(database_key) handler = lookup_connection_handler(role.to_sym) - connections << handler.establish_connection(db_config, default_pool_key, owner_name) + connections << handler.establish_connection(db_config, owner_name: owner_name) end - shards.each do |pool_key, database_keys| + shards.each do |shard, database_keys| database_keys.each do |role, database_key| db_config, owner_name = resolve_config_for_connection(database_key) handler = lookup_connection_handler(role.to_sym) - connections << handler.establish_connection(db_config, pool_key.to_sym, owner_name) + connections << handler.establish_connection(db_config, owner_name: owner_name, shard: shard.to_sym) end end @@ -154,7 +154,7 @@ module ActiveRecord db_config, owner_name = resolve_config_for_connection(database) handler = lookup_connection_handler(role) - handler.establish_connection(db_config, default_pool_key, owner_name) + handler.establish_connection(db_config, owner_name: owner_name) with_handler(role, &blk) elsif shard @@ -172,8 +172,8 @@ module ActiveRecord # ActiveRecord::Base.connected_to?(role: :writing) #=> true # ActiveRecord::Base.connected_to?(role: :reading) #=> false # end - def connected_to?(role:, shard: ActiveRecord::Base.default_pool_key) - current_role == role.to_sym && current_pool_key == shard.to_sym + def connected_to?(role:, shard: ActiveRecord::Base.default_shard) + current_role == role.to_sym && current_shard == shard.to_sym end # Returns the symbol representing the current connected role. @@ -247,16 +247,16 @@ module ActiveRecord end def connection_pool - connection_handler.retrieve_connection_pool(connection_specification_name, current_pool_key) || raise(ConnectionNotEstablished) + connection_handler.retrieve_connection_pool(connection_specification_name, shard: current_shard) || raise(ConnectionNotEstablished) end def retrieve_connection - connection_handler.retrieve_connection(connection_specification_name, current_pool_key) + connection_handler.retrieve_connection(connection_specification_name, shard: current_shard) end # Returns +true+ if Active Record is connected. def connected? - connection_handler.connected?(connection_specification_name, current_pool_key) + connection_handler.connected?(connection_specification_name, shard: current_shard) end def remove_connection(name = nil) @@ -264,11 +264,11 @@ module ActiveRecord # if removing a connection that has a pool, we reset the # connection_specification_name so it will use the parent # pool. - if connection_handler.retrieve_connection_pool(name, current_pool_key) + if connection_handler.retrieve_connection_pool(name, shard: current_shard) self.connection_specification_name = nil end - connection_handler.remove_connection_pool(name, current_pool_key) + connection_handler.remove_connection_pool(name, shard: current_shard) end def clear_cache! # :nodoc: @@ -302,15 +302,15 @@ module ActiveRecord end end - def with_shard(pool_key, role, prevent_writes) - old_pool_key = current_pool_key + def with_shard(shard, role, prevent_writes) + old_shard = current_shard with_role(role, prevent_writes) do - self.current_pool_key = pool_key + self.current_shard = shard yield end ensure - self.current_pool_key = old_pool_key + self.current_shard = old_shard end def swap_connection_handler(handler, &blk) # :nodoc: diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index e6cb21fc7f..cacc3ee95f 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -135,7 +135,7 @@ module ActiveRecord class_attribute :default_connection_handler, instance_writer: false - class_attribute :default_pool_key, instance_writer: false + class_attribute :default_shard, instance_writer: false self.filter_attributes = [] @@ -147,16 +147,16 @@ module ActiveRecord Thread.current.thread_variable_set(:ar_connection_handler, handler) end - def self.current_pool_key - Thread.current.thread_variable_get(:ar_pool_key) || default_pool_key + def self.current_shard + Thread.current.thread_variable_get(:ar_shard) || default_shard end - def self.current_pool_key=(pool_key) - Thread.current.thread_variable_set(:ar_pool_key, pool_key) + def self.current_shard=(shard) + Thread.current.thread_variable_set(:ar_shard, shard) end self.default_connection_handler = ConnectionAdapters::ConnectionHandler.new - self.default_pool_key = :default + self.default_shard = :default end module ClassMethods diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index f7aebc5ecb..c243bcc44d 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -136,7 +136,6 @@ module ActiveRecord end def deserialize(value) - return if value.nil? mapping.key(subtype.deserialize(value)) end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 88e8c583bf..56c7495226 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -1376,20 +1376,29 @@ module ActiveRecord def with_advisory_lock lock_id = generate_migrator_advisory_lock_id - AdvisoryLockBase.establish_connection(ActiveRecord::Base.connection_db_config) unless AdvisoryLockBase.connected? - connection = AdvisoryLockBase.connection - got_lock = connection.get_advisory_lock(lock_id) - raise ConcurrentMigrationError unless got_lock - load_migrated # reload schema_migrations to be sure it wasn't changed by another process before we got the lock - yield - ensure - if got_lock && !connection.release_advisory_lock(lock_id) - raise ConcurrentMigrationError.new( - ConcurrentMigrationError::RELEASE_LOCK_FAILED_MESSAGE - ) + + with_advisory_lock_connection do |connection| + got_lock = connection.get_advisory_lock(lock_id) + raise ConcurrentMigrationError unless got_lock + load_migrated # reload schema_migrations to be sure it wasn't changed by another process before we got the lock + yield + ensure + if got_lock && !connection.release_advisory_lock(lock_id) + raise ConcurrentMigrationError.new( + ConcurrentMigrationError::RELEASE_LOCK_FAILED_MESSAGE + ) + end end end + def with_advisory_lock_connection + pool = ActiveRecord::ConnectionAdapters::ConnectionHandler.new.establish_connection( + ActiveRecord::Base.connection_db_config + ) + + pool.with_connection { |connection| yield(connection) } + end + MIGRATOR_SALT = 2053462845 def generate_migrator_advisory_lock_id db_name_hash = Zlib.crc32(Base.connection.current_database) diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 005707f94a..bd1f438e57 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -1252,7 +1252,7 @@ module ActiveRecord end joins.each_with_index do |join, i| - joins[i] = table.create_string_join(Arel.sql(join.strip)) if join.is_a?(String) + joins[i] = Arel::Nodes::StringJoin.new(Arel.sql(join.strip)) if join.is_a?(String) end while joins.first.is_a?(Arel::Nodes::Join) diff --git a/activerecord/lib/active_record/runtime_registry.rb b/activerecord/lib/active_record/runtime_registry.rb index 4975cb8967..7363d2d7a0 100644 --- a/activerecord/lib/active_record/runtime_registry.rb +++ b/activerecord/lib/active_record/runtime_registry.rb @@ -14,9 +14,9 @@ module ActiveRecord class RuntimeRegistry # :nodoc: extend ActiveSupport::PerThreadRegistry - attr_accessor :connection_handler, :sql_runtime + attr_accessor :sql_runtime - [:connection_handler, :sql_runtime].each do |val| + [:sql_runtime].each do |val| class_eval %{ def self.#{val}; instance.#{val}; end }, __FILE__, __LINE__ class_eval %{ def self.#{val}=(x); instance.#{val}=x; end }, __FILE__, __LINE__ end diff --git a/activerecord/test/assets/schema_dump_5_1.yml b/activerecord/test/assets/schema_dump_5_1.yml index f37977daf2..9563b7e100 100644 --- a/activerecord/test/assets/schema_dump_5_1.yml +++ b/activerecord/test/assets/schema_dump_5_1.yml @@ -316,7 +316,7 @@ data_sources: triangles: true non_poly_ones: true non_poly_twos: true - men: true + humans: true faces: true interests: true zines: true diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index de9742b250..9a4822aae5 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -120,7 +120,7 @@ class Sink < ActiveRecord::Base end class Source < ActiveRecord::Base - self.table_name = "men" + self.table_name = "humans" has_and_belongs_to_many :sinks, join_table: :edges end 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 284075fb5c..409eaf6d02 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -1069,6 +1069,18 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal expected, Author.eager_load(:lazy_readers_skimmers_or_not_2).last.lazy_readers_skimmers_or_not_2 end + def test_duplicated_has_many_through_with_join_scope + Categorization.create!(author: authors(:david), post: posts(:thinking), category: categories(:technology)) + + expected = [categorizations(:david_welcome_general)] + assert_equal expected, Author.preload(:general_posts, :general_categorizations).first.general_categorizations + assert_equal expected, Author.eager_load(:general_posts, :general_categorizations).first.general_categorizations + + expected = [posts(:welcome)] + assert_equal expected, Author.preload(:general_categorizations, :general_posts).first.general_posts + assert_equal expected, Author.eager_load(:general_categorizations, :general_posts).first.general_posts + end + def test_has_many_through_polymorphic_with_rewhere post = TaggedPost.create!(title: "Tagged", body: "Post") tag = post.tags.create!(name: "Tag") diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index 4d71e71c8b..b9ee7c8639 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require "cases/helper" -require "models/man" +require "models/human" require "models/face" require "models/interest" require "models/zine" @@ -26,14 +26,14 @@ class AutomaticInverseFindingTests < ActiveRecord::TestCase fixtures :ratings, :comments, :cars def test_has_one_and_belongs_to_should_find_inverse_automatically_on_multiple_word_name - monkey_reflection = MixedCaseMonkey.reflect_on_association(:man) - man_reflection = Man.reflect_on_association(:mixed_case_monkey) + monkey_reflection = MixedCaseMonkey.reflect_on_association(:human) + human_reflection = Human.reflect_on_association(:mixed_case_monkey) assert monkey_reflection.has_inverse?, "The monkey reflection should have an inverse" - assert_equal man_reflection, monkey_reflection.inverse_of, "The monkey reflection's inverse should be the man reflection" + assert_equal human_reflection, monkey_reflection.inverse_of, "The monkey reflection's inverse should be the human reflection" - assert man_reflection.has_inverse?, "The man reflection should have an inverse" - assert_equal monkey_reflection, man_reflection.inverse_of, "The man reflection's inverse should be the monkey reflection" + assert human_reflection.has_inverse?, "The human reflection should have an inverse" + assert_equal monkey_reflection, human_reflection.inverse_of, "The human reflection's inverse should be the monkey reflection" end def test_has_many_and_belongs_to_should_find_inverse_automatically_for_model_in_module @@ -136,9 +136,9 @@ class AutomaticInverseFindingTests < ActiveRecord::TestCase end def test_polymorphic_has_one_should_find_inverse_automatically - man_reflection = Man.reflect_on_association(:polymorphic_face_without_inverse) + human_reflection = Human.reflect_on_association(:polymorphic_face_without_inverse) - assert_predicate man_reflection, :has_inverse? + assert_predicate human_reflection, :has_inverse? end end @@ -158,13 +158,13 @@ class InverseAssociationTests < ActiveRecord::TestCase end def test_should_be_able_to_ask_a_reflection_if_it_has_an_inverse - has_one_with_inverse_ref = Man.reflect_on_association(:face) + has_one_with_inverse_ref = Human.reflect_on_association(:face) assert_predicate has_one_with_inverse_ref, :has_inverse? - has_many_with_inverse_ref = Man.reflect_on_association(:interests) + has_many_with_inverse_ref = Human.reflect_on_association(:interests) assert_predicate has_many_with_inverse_ref, :has_inverse? - belongs_to_with_inverse_ref = Face.reflect_on_association(:man) + belongs_to_with_inverse_ref = Face.reflect_on_association(:human) assert_predicate belongs_to_with_inverse_ref, :has_inverse? has_one_without_inverse_ref = Club.reflect_on_association(:sponsor) @@ -178,14 +178,14 @@ class InverseAssociationTests < ActiveRecord::TestCase end def test_inverse_of_method_should_supply_the_actual_reflection_instance_it_is_the_inverse_of - has_one_ref = Man.reflect_on_association(:face) - assert_equal Face.reflect_on_association(:man), has_one_ref.inverse_of + has_one_ref = Human.reflect_on_association(:face) + assert_equal Face.reflect_on_association(:human), has_one_ref.inverse_of - has_many_ref = Man.reflect_on_association(:interests) - assert_equal Interest.reflect_on_association(:man), has_many_ref.inverse_of + has_many_ref = Human.reflect_on_association(:interests) + assert_equal Interest.reflect_on_association(:human), has_many_ref.inverse_of - belongs_to_ref = Face.reflect_on_association(:man) - assert_equal Man.reflect_on_association(:face), belongs_to_ref.inverse_of + belongs_to_ref = Face.reflect_on_association(:human) + assert_equal Human.reflect_on_association(:face), belongs_to_ref.inverse_of end def test_associations_with_no_inverse_of_should_return_nil @@ -204,7 +204,7 @@ class InverseAssociationTests < ActiveRecord::TestCase assert_raise(ArgumentError) { belongs_to_ref.klass } assert_nil belongs_to_ref.inverse_of - belongs_to_ref = Face.reflect_on_association(:human) + belongs_to_ref = Face.reflect_on_association(:super_human) assert_raise(ArgumentError) { belongs_to_ref.klass } assert_nil belongs_to_ref.inverse_of end @@ -221,269 +221,269 @@ class InverseAssociationTests < ActiveRecord::TestCase end class InverseHasOneTests < ActiveRecord::TestCase - fixtures :men, :faces + fixtures :humans, :faces def test_parent_instance_should_be_shared_with_child_on_find - m = men(:gordon) - f = m.face - assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance" - m.name = "Bongo" - assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance" - f.man.name = "Mungo" - assert_equal m.name, f.man.name, "Name of man should be the same after changes to child-owned instance" + human = humans(:gordon) + face = human.face + assert_equal human.name, face.human.name, "Name of human should be the same before changes to parent instance" + human.name = "Bongo" + assert_equal human.name, face.human.name, "Name of human should be the same after changes to parent instance" + face.human.name = "Mungo" + assert_equal human.name, face.human.name, "Name of human should be the same after changes to child-owned instance" end def test_parent_instance_should_be_shared_with_eager_loaded_child_on_find - m = Man.all.merge!(where: { name: "Gordon" }, includes: :face).first - f = m.face - assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance" - m.name = "Bongo" - assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance" - f.man.name = "Mungo" - assert_equal m.name, f.man.name, "Name of man should be the same after changes to child-owned instance" + human = Human.all.merge!(where: { name: "Gordon" }, includes: :face).first + face = human.face + assert_equal human.name, face.human.name, "Name of human should be the same before changes to parent instance" + human.name = "Bongo" + assert_equal human.name, face.human.name, "Name of human should be the same after changes to parent instance" + face.human.name = "Mungo" + assert_equal human.name, face.human.name, "Name of human should be the same after changes to child-owned instance" - m = Man.all.merge!(where: { name: "Gordon" }, includes: :face, order: "faces.id").first - f = m.face - assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance" - m.name = "Bongo" - assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance" - f.man.name = "Mungo" - assert_equal m.name, f.man.name, "Name of man should be the same after changes to child-owned instance" + human = Human.all.merge!(where: { name: "Gordon" }, includes: :face, order: "faces.id").first + face = human.face + assert_equal human.name, face.human.name, "Name of human should be the same before changes to parent instance" + human.name = "Bongo" + assert_equal human.name, face.human.name, "Name of human should be the same after changes to parent instance" + face.human.name = "Mungo" + assert_equal human.name, face.human.name, "Name of human should be the same after changes to child-owned instance" end def test_parent_instance_should_be_shared_with_newly_built_child - m = Man.first - f = m.build_face(description: "haunted") - assert_not_nil f.man - assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance" - m.name = "Bongo" - assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance" - f.man.name = "Mungo" - assert_equal m.name, f.man.name, "Name of man should be the same after changes to just-built-child-owned instance" + human = Human.first + face = human.build_face(description: "haunted") + assert_not_nil face.human + assert_equal human.name, face.human.name, "Name of human should be the same before changes to parent instance" + human.name = "Bongo" + assert_equal human.name, face.human.name, "Name of human should be the same after changes to parent instance" + face.human.name = "Mungo" + assert_equal human.name, face.human.name, "Name of human should be the same after changes to just-built-child-owned instance" end def test_parent_instance_should_be_shared_with_newly_created_child - m = Man.first - f = m.create_face(description: "haunted") - assert_not_nil f.man - assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance" - m.name = "Bongo" - assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance" - f.man.name = "Mungo" - assert_equal m.name, f.man.name, "Name of man should be the same after changes to newly-created-child-owned instance" + human = Human.first + face = human.create_face(description: "haunted") + assert_not_nil face.human + assert_equal human.name, face.human.name, "Name of human should be the same before changes to parent instance" + human.name = "Bongo" + assert_equal human.name, face.human.name, "Name of human should be the same after changes to parent instance" + face.human.name = "Mungo" + assert_equal human.name, face.human.name, "Name of human should be the same after changes to newly-created-child-owned instance" end def test_parent_instance_should_be_shared_with_newly_created_child_via_bang_method - m = Man.first - f = m.create_face!(description: "haunted") - assert_not_nil f.man - assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance" - m.name = "Bongo" - assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance" - f.man.name = "Mungo" - assert_equal m.name, f.man.name, "Name of man should be the same after changes to newly-created-child-owned instance" + human = Human.first + face = human.create_face!(description: "haunted") + assert_not_nil face.human + assert_equal human.name, face.human.name, "Name of human should be the same before changes to parent instance" + human.name = "Bongo" + assert_equal human.name, face.human.name, "Name of human should be the same after changes to parent instance" + face.human.name = "Mungo" + assert_equal human.name, face.human.name, "Name of human should be the same after changes to newly-created-child-owned instance" end def test_parent_instance_should_be_shared_with_replaced_via_accessor_child - m = Man.first - f = Face.new(description: "haunted") - m.face = f - assert_not_nil f.man - assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance" - m.name = "Bongo" - assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance" - f.man.name = "Mungo" - assert_equal m.name, f.man.name, "Name of man should be the same after changes to replaced-child-owned instance" + human = Human.first + face = Face.new(description: "haunted") + human.face = face + assert_not_nil face.human + assert_equal human.name, face.human.name, "Name of human should be the same before changes to parent instance" + human.name = "Bongo" + assert_equal human.name, face.human.name, "Name of human should be the same after changes to parent instance" + face.human.name = "Mungo" + assert_equal human.name, face.human.name, "Name of human should be the same after changes to replaced-child-owned instance" end def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error - assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Man.first.dirty_face } + assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Human.first.dirty_face } end end class InverseHasManyTests < ActiveRecord::TestCase - fixtures :men, :interests, :posts, :authors, :author_addresses + fixtures :humans, :interests, :posts, :authors, :author_addresses def test_parent_instance_should_be_shared_with_every_child_on_find - m = men(:gordon) - is = m.interests - is.each do |i| - assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance" - m.name = "Bongo" - assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance" - i.man.name = "Mungo" - assert_equal m.name, i.man.name, "Name of man should be the same after changes to child-owned instance" + human = humans(:gordon) + interests = human.interests + interests.each do |interest| + assert_equal human.name, interest.human.name, "Name of human should be the same before changes to parent instance" + human.name = "Bongo" + assert_equal human.name, interest.human.name, "Name of human should be the same after changes to parent instance" + interest.human.name = "Mungo" + assert_equal human.name, interest.human.name, "Name of human should be the same after changes to child-owned instance" end end def test_parent_instance_should_be_shared_with_every_child_on_find_for_sti - a = authors(:david) - ps = a.posts - ps.each do |p| - assert_equal a.name, p.author.name, "Name of man should be the same before changes to parent instance" - a.name = "Bongo" - assert_equal a.name, p.author.name, "Name of man should be the same after changes to parent instance" - p.author.name = "Mungo" - assert_equal a.name, p.author.name, "Name of man should be the same after changes to child-owned instance" + author = authors(:david) + posts = author.posts + posts.each do |post| + assert_equal author.name, post.author.name, "Name of human should be the same before changes to parent instance" + author.name = "Bongo" + assert_equal author.name, post.author.name, "Name of human should be the same after changes to parent instance" + post.author.name = "Mungo" + assert_equal author.name, post.author.name, "Name of human should be the same after changes to child-owned instance" end - sps = a.special_posts - sps.each do |sp| - assert_equal a.name, sp.author.name, "Name of man should be the same before changes to parent instance" - a.name = "Bongo" - assert_equal a.name, sp.author.name, "Name of man should be the same after changes to parent instance" - sp.author.name = "Mungo" - assert_equal a.name, sp.author.name, "Name of man should be the same after changes to child-owned instance" + special_posts = author.special_posts + special_posts.each do |post| + assert_equal author.name, post.author.name, "Name of human should be the same before changes to parent instance" + author.name = "Bongo" + assert_equal author.name, post.author.name, "Name of human should be the same after changes to parent instance" + post.author.name = "Mungo" + assert_equal author.name, post.author.name, "Name of human should be the same after changes to child-owned instance" end end def test_parent_instance_should_be_shared_with_eager_loaded_children - m = Man.all.merge!(where: { name: "Gordon" }, includes: :interests).first - is = m.interests - is.each do |i| - assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance" - m.name = "Bongo" - assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance" - i.man.name = "Mungo" - assert_equal m.name, i.man.name, "Name of man should be the same after changes to child-owned instance" + human = Human.all.merge!(where: { name: "Gordon" }, includes: :interests).first + interests = human.interests + interests.each do |interest| + assert_equal human.name, interest.human.name, "Name of human should be the same before changes to parent instance" + human.name = "Bongo" + assert_equal human.name, interest.human.name, "Name of human should be the same after changes to parent instance" + interest.human.name = "Mungo" + assert_equal human.name, interest.human.name, "Name of human should be the same after changes to child-owned instance" end - m = Man.all.merge!(where: { name: "Gordon" }, includes: :interests, order: "interests.id").first - is = m.interests - is.each do |i| - assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance" - m.name = "Bongo" - assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance" - i.man.name = "Mungo" - assert_equal m.name, i.man.name, "Name of man should be the same after changes to child-owned instance" + human = Human.all.merge!(where: { name: "Gordon" }, includes: :interests, order: "interests.id").first + interests = human.interests + interests.each do |interest| + assert_equal human.name, interest.human.name, "Name of human should be the same before changes to parent instance" + human.name = "Bongo" + assert_equal human.name, interest.human.name, "Name of human should be the same after changes to parent instance" + interest.human.name = "Mungo" + assert_equal human.name, interest.human.name, "Name of human should be the same after changes to child-owned instance" end end def test_parent_instance_should_be_shared_with_newly_block_style_built_child - m = Man.first - i = m.interests.build { |ii| ii.topic = "Industrial Revolution Re-enactment" } - assert_not_nil i.topic, "Child attributes supplied to build via blocks should be populated" - assert_not_nil i.man - assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance" - m.name = "Bongo" - assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance" - i.man.name = "Mungo" - assert_equal m.name, i.man.name, "Name of man should be the same after changes to just-built-child-owned instance" + human = Human.first + interest = human.interests.build { |ii| ii.topic = "Industrial Revolution Re-enactment" } + assert_not_nil interest.topic, "Child attributes supplied to build via blocks should be populated" + assert_not_nil interest.human + assert_equal human.name, interest.human.name, "Name of human should be the same before changes to parent instance" + human.name = "Bongo" + assert_equal human.name, interest.human.name, "Name of human should be the same after changes to parent instance" + interest.human.name = "Mungo" + assert_equal human.name, interest.human.name, "Name of human should be the same after changes to just-built-child-owned instance" end def test_parent_instance_should_be_shared_with_newly_created_via_bang_method_child - m = Man.first - i = m.interests.create!(topic: "Industrial Revolution Re-enactment") - assert_not_nil i.man - assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance" - m.name = "Bongo" - assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance" - i.man.name = "Mungo" - assert_equal m.name, i.man.name, "Name of man should be the same after changes to newly-created-child-owned instance" + human = Human.first + interest = human.interests.create!(topic: "Industrial Revolution Re-enactment") + assert_not_nil interest.human + assert_equal human.name, interest.human.name, "Name of human should be the same before changes to parent instance" + human.name = "Bongo" + assert_equal human.name, interest.human.name, "Name of human should be the same after changes to parent instance" + interest.human.name = "Mungo" + assert_equal human.name, interest.human.name, "Name of human should be the same after changes to newly-created-child-owned instance" end def test_parent_instance_should_be_shared_with_newly_block_style_created_child - m = Man.first - i = m.interests.create { |ii| ii.topic = "Industrial Revolution Re-enactment" } - assert_not_nil i.topic, "Child attributes supplied to create via blocks should be populated" - assert_not_nil i.man - assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance" - m.name = "Bongo" - assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance" - i.man.name = "Mungo" - assert_equal m.name, i.man.name, "Name of man should be the same after changes to newly-created-child-owned instance" + human = Human.first + interest = human.interests.create { |ii| ii.topic = "Industrial Revolution Re-enactment" } + assert_not_nil interest.topic, "Child attributes supplied to create via blocks should be populated" + assert_not_nil interest.human + assert_equal human.name, interest.human.name, "Name of human should be the same before changes to parent instance" + human.name = "Bongo" + assert_equal human.name, interest.human.name, "Name of human should be the same after changes to parent instance" + interest.human.name = "Mungo" + assert_equal human.name, interest.human.name, "Name of human should be the same after changes to newly-created-child-owned instance" end def test_parent_instance_should_be_shared_within_create_block_of_new_child - man = Man.first - interest = man.interests.create do |i| - assert i.man.equal?(man), "Man of child should be the same instance as a parent" + human = Human.first + interest = human.interests.create do |i| + assert i.human.equal?(human), "Human of child should be the same instance as a parent" end - assert interest.man.equal?(man), "Man of the child should still be the same instance as a parent" + assert interest.human.equal?(human), "Human of the child should still be the same instance as a parent" end def test_parent_instance_should_be_shared_within_build_block_of_new_child - man = Man.first - interest = man.interests.build do |i| - assert i.man.equal?(man), "Man of child should be the same instance as a parent" + human = Human.first + interest = human.interests.build do |i| + assert i.human.equal?(human), "Human of child should be the same instance as a parent" end - assert interest.man.equal?(man), "Man of the child should still be the same instance as a parent" + assert interest.human.equal?(human), "Human of the child should still be the same instance as a parent" end def test_parent_instance_should_be_shared_with_poked_in_child - m = men(:gordon) - i = Interest.create(topic: "Industrial Revolution Re-enactment") - m.interests << i - assert_not_nil i.man - assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance" - m.name = "Bongo" - assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance" - i.man.name = "Mungo" - assert_equal m.name, i.man.name, "Name of man should be the same after changes to newly-created-child-owned instance" + human = humans(:gordon) + interest = Interest.create(topic: "Industrial Revolution Re-enactment") + human.interests << interest + assert_not_nil interest.human + assert_equal human.name, interest.human.name, "Name of human should be the same before changes to parent instance" + human.name = "Bongo" + assert_equal human.name, interest.human.name, "Name of human should be the same after changes to parent instance" + interest.human.name = "Mungo" + assert_equal human.name, interest.human.name, "Name of human should be the same after changes to newly-created-child-owned instance" end def test_parent_instance_should_be_shared_with_replaced_via_accessor_children - m = Man.first - i = Interest.new(topic: "Industrial Revolution Re-enactment") - m.interests = [i] - assert_not_nil i.man - assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance" - m.name = "Bongo" - assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance" - i.man.name = "Mungo" - assert_equal m.name, i.man.name, "Name of man should be the same after changes to replaced-child-owned instance" + human = Human.first + interest = Interest.new(topic: "Industrial Revolution Re-enactment") + human.interests = [interest] + assert_not_nil interest.human + assert_equal human.name, interest.human.name, "Name of human should be the same before changes to parent instance" + human.name = "Bongo" + assert_equal human.name, interest.human.name, "Name of human should be the same after changes to parent instance" + interest.human.name = "Mungo" + assert_equal human.name, interest.human.name, "Name of human should be the same after changes to replaced-child-owned instance" end def test_parent_instance_should_be_shared_with_first_and_last_child - man = Man.first + human = Human.first - assert man.interests.first.man.equal? man - assert man.interests.last.man.equal? man + assert human.interests.first.human.equal? human + assert human.interests.last.human.equal? human end def test_parent_instance_should_be_shared_with_first_n_and_last_n_children - man = Man.first + human = Human.first - interests = man.interests.first(2) - assert interests[0].man.equal? man - assert interests[1].man.equal? man + interests = human.interests.first(2) + assert interests[0].human.equal? human + assert interests[1].human.equal? human - interests = man.interests.last(2) - assert interests[0].man.equal? man - assert interests[1].man.equal? man + interests = human.interests.last(2) + assert interests[0].human.equal? human + assert interests[1].human.equal? human end def test_parent_instance_should_find_child_instance_using_child_instance_id - man = Man.create! + human = Human.create! interest = Interest.create! - man.interests = [interest] + human.interests = [interest] - assert interest.equal?(man.interests.first), "The inverse association should use the interest already created and held in memory" - assert interest.equal?(man.interests.find(interest.id)), "The inverse association should use the interest already created and held in memory" - assert man.equal?(man.interests.first.man), "Two inversion should lead back to the same object that was originally held" - assert man.equal?(man.interests.find(interest.id).man), "Two inversions should lead back to the same object that was originally held" + assert interest.equal?(human.interests.first), "The inverse association should use the interest already created and held in memory" + assert interest.equal?(human.interests.find(interest.id)), "The inverse association should use the interest already created and held in memory" + assert human.equal?(human.interests.first.human), "Two inversion should lead back to the same object that was originally held" + assert human.equal?(human.interests.find(interest.id).human), "Two inversions should lead back to the same object that was originally held" end def test_parent_instance_should_find_child_instance_using_child_instance_id_when_created - man = Man.create! - interest = Interest.create!(man: man) + human = Human.create! + interest = Interest.create!(human: human) - assert man.equal?(man.interests.first.man), "Two inverses should lead back to the same object that was originally held" - assert man.equal?(man.interests.find(interest.id).man), "Two inversions should lead back to the same object that was originally held" + assert human.equal?(human.interests.first.human), "Two inverses should lead back to the same object that was originally held" + assert human.equal?(human.interests.find(interest.id).human), "Two inversions should lead back to the same object that was originally held" - assert_nil man.interests.find(interest.id).man.name, "The name of the man should match before the name is changed" - man.name = "Ben Bitdiddle" - assert_equal man.name, man.interests.find(interest.id).man.name, "The name of the man should match after the parent name is changed" - man.interests.find(interest.id).man.name = "Alyssa P. Hacker" - assert_equal man.name, man.interests.find(interest.id).man.name, "The name of the man should match after the child name is changed" + assert_nil human.interests.find(interest.id).human.name, "The name of the human should match before the name is changed" + human.name = "Ben Bitdiddle" + assert_equal human.name, human.interests.find(interest.id).human.name, "The name of the human should match after the parent name is changed" + human.interests.find(interest.id).human.name = "Alyssa P. Hacker" + assert_equal human.name, human.interests.find(interest.id).human.name, "The name of the human should match after the child name is changed" end def test_find_on_child_instance_with_id_should_not_load_all_child_records - man = Man.create! - interest = Interest.create!(man: man) + human = Human.create! + interest = Interest.create!(human: human) - man.interests.find(interest.id) - assert_not_predicate man.interests, :loaded? + human.interests.find(interest.id) + assert_not_predicate human.interests, :loaded? end def test_raise_record_not_found_error_when_invalid_ids_are_passed @@ -491,316 +491,316 @@ class InverseHasManyTests < ActiveRecord::TestCase # are indeed invalid. Interest.delete_all - man = Man.create! + human = Human.create! invalid_id = 245324523 - assert_raise(ActiveRecord::RecordNotFound) { man.interests.find(invalid_id) } + assert_raise(ActiveRecord::RecordNotFound) { human.interests.find(invalid_id) } invalid_ids = [8432342, 2390102913, 2453245234523452] - assert_raise(ActiveRecord::RecordNotFound) { man.interests.find(invalid_ids) } + assert_raise(ActiveRecord::RecordNotFound) { human.interests.find(invalid_ids) } end def test_raise_record_not_found_error_when_no_ids_are_passed - man = Man.create! + human = Human.create! - exception = assert_raise(ActiveRecord::RecordNotFound) { man.interests.load.find() } + exception = assert_raise(ActiveRecord::RecordNotFound) { human.interests.load.find() } assert_equal exception.model, "Interest" assert_equal exception.primary_key, "id" end def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error - assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Man.first.secret_interests } + assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Human.first.secret_interests } end def test_child_instance_should_point_to_parent_without_saving - man = Man.new - i = Interest.create(topic: "Industrial Revolution Re-enactment") + human = Human.new + interest = Interest.create(topic: "Industrial Revolution Re-enactment") - man.interests << i - assert_not_nil i.man + human.interests << interest + assert_not_nil interest.human - i.man.name = "Charles" - assert_equal i.man.name, man.name + interest.human.name = "Charles" + assert_equal interest.human.name, human.name - assert_not_predicate man, :persisted? + assert_not_predicate human, :persisted? end def test_inverse_instance_should_be_set_before_find_callbacks_are_run reset_callbacks(Interest, :find) do - Interest.after_find { raise unless association(:man).loaded? && man.present? } + Interest.after_find { raise unless association(:human).loaded? && human.present? } - assert_predicate Man.first.interests.reload, :any? - assert_predicate Man.includes(:interests).first.interests, :any? - assert_predicate Man.joins(:interests).includes(:interests).first.interests, :any? + assert_predicate Human.first.interests.reload, :any? + assert_predicate Human.includes(:interests).first.interests, :any? + assert_predicate Human.joins(:interests).includes(:interests).first.interests, :any? end end def test_inverse_instance_should_be_set_before_initialize_callbacks_are_run reset_callbacks(Interest, :initialize) do - Interest.after_initialize { raise unless association(:man).loaded? && man.present? } + Interest.after_initialize { raise unless association(:human).loaded? && human.present? } - assert_predicate Man.first.interests.reload, :any? - assert_predicate Man.includes(:interests).first.interests, :any? - assert_predicate Man.joins(:interests).includes(:interests).first.interests, :any? + assert_predicate Human.first.interests.reload, :any? + assert_predicate Human.includes(:interests).first.interests, :any? + assert_predicate Human.joins(:interests).includes(:interests).first.interests, :any? end end end class InverseBelongsToTests < ActiveRecord::TestCase - fixtures :men, :faces, :interests + fixtures :humans, :faces, :interests def test_child_instance_should_be_shared_with_parent_on_find - f = faces(:trusting) - m = f.man - assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance" - f.description = "gormless" - assert_equal f.description, m.face.description, "Description of face should be the same after changes to child instance" - m.face.description = "pleasing" - assert_equal f.description, m.face.description, "Description of face should be the same after changes to parent-owned instance" + face = faces(:trusting) + human = face.human + assert_equal face.description, human.face.description, "Description of face should be the same before changes to child instance" + face.description = "gormless" + assert_equal face.description, human.face.description, "Description of face should be the same after changes to child instance" + human.face.description = "pleasing" + assert_equal face.description, human.face.description, "Description of face should be the same after changes to parent-owned instance" end def test_eager_loaded_child_instance_should_be_shared_with_parent_on_find - f = Face.all.merge!(includes: :man, where: { description: "trusting" }).first - m = f.man - assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance" - f.description = "gormless" - assert_equal f.description, m.face.description, "Description of face should be the same after changes to child instance" - m.face.description = "pleasing" - assert_equal f.description, m.face.description, "Description of face should be the same after changes to parent-owned instance" + face = Face.all.merge!(includes: :human, where: { description: "trusting" }).first + human = face.human + assert_equal face.description, human.face.description, "Description of face should be the same before changes to child instance" + face.description = "gormless" + assert_equal face.description, human.face.description, "Description of face should be the same after changes to child instance" + human.face.description = "pleasing" + assert_equal face.description, human.face.description, "Description of face should be the same after changes to parent-owned instance" - f = Face.all.merge!(includes: :man, order: "men.id", where: { description: "trusting" }).first - m = f.man - assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance" - f.description = "gormless" - assert_equal f.description, m.face.description, "Description of face should be the same after changes to child instance" - m.face.description = "pleasing" - assert_equal f.description, m.face.description, "Description of face should be the same after changes to parent-owned instance" + face = Face.all.merge!(includes: :human, order: "humans.id", where: { description: "trusting" }).first + human = face.human + assert_equal face.description, human.face.description, "Description of face should be the same before changes to child instance" + face.description = "gormless" + assert_equal face.description, human.face.description, "Description of face should be the same after changes to child instance" + human.face.description = "pleasing" + assert_equal face.description, human.face.description, "Description of face should be the same after changes to parent-owned instance" end def test_child_instance_should_be_shared_with_newly_built_parent - f = faces(:trusting) - m = f.build_man(name: "Charles") - assert_not_nil m.face - assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance" - f.description = "gormless" - assert_equal f.description, m.face.description, "Description of face should be the same after changes to child instance" - m.face.description = "pleasing" - assert_equal f.description, m.face.description, "Description of face should be the same after changes to just-built-parent-owned instance" + face = faces(:trusting) + human = face.build_human(name: "Charles") + assert_not_nil human.face + assert_equal face.description, human.face.description, "Description of face should be the same before changes to child instance" + face.description = "gormless" + assert_equal face.description, human.face.description, "Description of face should be the same after changes to child instance" + human.face.description = "pleasing" + assert_equal face.description, human.face.description, "Description of face should be the same after changes to just-built-parent-owned instance" end def test_child_instance_should_be_shared_with_newly_created_parent - f = faces(:trusting) - m = f.create_man(name: "Charles") - assert_not_nil m.face - assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance" - f.description = "gormless" - assert_equal f.description, m.face.description, "Description of face should be the same after changes to child instance" - m.face.description = "pleasing" - assert_equal f.description, m.face.description, "Description of face should be the same after changes to newly-created-parent-owned instance" + face = faces(:trusting) + human = face.create_human(name: "Charles") + assert_not_nil human.face + assert_equal face.description, human.face.description, "Description of face should be the same before changes to child instance" + face.description = "gormless" + assert_equal face.description, human.face.description, "Description of face should be the same after changes to child instance" + human.face.description = "pleasing" + assert_equal face.description, human.face.description, "Description of face should be the same after changes to newly-created-parent-owned instance" end def test_should_not_try_to_set_inverse_instances_when_the_inverse_is_a_has_many - i = interests(:trainspotting) - m = i.man - assert_not_nil m.interests - iz = m.interests.detect { |_iz| _iz.id == i.id } + interest = interests(:trainspotting) + human = interest.human + assert_not_nil human.interests + iz = human.interests.detect { |_iz| _iz.id == interest.id } assert_not_nil iz - assert_equal i.topic, iz.topic, "Interest topics should be the same before changes to child" - i.topic = "Eating cheese with a spoon" - assert_not_equal i.topic, iz.topic, "Interest topics should not be the same after changes to child" + assert_equal interest.topic, iz.topic, "Interest topics should be the same before changes to child" + interest.topic = "Eating cheese with a spoon" + assert_not_equal interest.topic, iz.topic, "Interest topics should not be the same after changes to child" iz.topic = "Cow tipping" - assert_not_equal i.topic, iz.topic, "Interest topics should not be the same after changes to parent-owned instance" + assert_not_equal interest.topic, iz.topic, "Interest topics should not be the same after changes to parent-owned instance" end def test_with_has_many_inversing_should_try_to_set_inverse_instances_when_the_inverse_is_a_has_many with_has_many_inversing do - i = interests(:trainspotting) - m = i.man - assert_not_nil m.interests - iz = m.interests.detect { |_iz| _iz.id == i.id } + interest = interests(:trainspotting) + human = interest.human + assert_not_nil human.interests + iz = human.interests.detect { |_iz| _iz.id == interest.id } assert_not_nil iz - assert_equal i.topic, iz.topic, "Interest topics should be the same before changes to child" - i.topic = "Eating cheese with a spoon" - assert_equal i.topic, iz.topic, "Interest topics should be the same after changes to child" + assert_equal interest.topic, iz.topic, "Interest topics should be the same before changes to child" + interest.topic = "Eating cheese with a spoon" + assert_equal interest.topic, iz.topic, "Interest topics should be the same after changes to child" iz.topic = "Cow tipping" - assert_equal i.topic, iz.topic, "Interest topics should be the same after changes to parent-owned instance" + assert_equal interest.topic, iz.topic, "Interest topics should be the same after changes to parent-owned instance" end end def test_with_has_many_inversing_does_not_trigger_association_callbacks_on_set_when_the_inverse_is_a_has_many with_has_many_inversing do - man = interests(:trainspotting).man_with_callbacks - assert_not_predicate man, :add_callback_called? + human = interests(:trainspotting).human_with_callbacks + assert_not_predicate human, :add_callback_called? end end def test_child_instance_should_be_shared_with_replaced_via_accessor_parent - f = Face.first - m = Man.new(name: "Charles") - f.man = m - assert_not_nil m.face - assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance" - f.description = "gormless" - assert_equal f.description, m.face.description, "Description of face should be the same after changes to child instance" - m.face.description = "pleasing" - assert_equal f.description, m.face.description, "Description of face should be the same after changes to replaced-parent-owned instance" + face = Face.first + human = Human.new(name: "Charles") + face.human = human + assert_not_nil human.face + assert_equal face.description, human.face.description, "Description of face should be the same before changes to child instance" + face.description = "gormless" + assert_equal face.description, human.face.description, "Description of face should be the same after changes to child instance" + human.face.description = "pleasing" + assert_equal face.description, human.face.description, "Description of face should be the same after changes to replaced-parent-owned instance" end def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error - assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.horrible_man } + assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.horrible_human } end def test_building_has_many_parent_association_inverses_one_record with_has_many_inversing do interest = Interest.new - interest.build_man - assert_equal 1, interest.man.interests.size + interest.build_human + assert_equal 1, interest.human.interests.size interest.save! - assert_equal 1, interest.man.interests.size + assert_equal 1, interest.human.interests.size end end end class InversePolymorphicBelongsToTests < ActiveRecord::TestCase - fixtures :men, :faces, :interests + fixtures :humans, :faces, :interests def test_child_instance_should_be_shared_with_parent_on_find - f = Face.all.merge!(where: { description: "confused" }).first - m = f.polymorphic_man - assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same before changes to child instance" - f.description = "gormless" - assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to child instance" - m.polymorphic_face.description = "pleasing" - assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to parent-owned instance" + face = Face.all.merge!(where: { description: "confused" }).first + human = face.polymorphic_human + assert_equal face.description, human.polymorphic_face.description, "Description of face should be the same before changes to child instance" + face.description = "gormless" + assert_equal face.description, human.polymorphic_face.description, "Description of face should be the same after changes to child instance" + human.polymorphic_face.description = "pleasing" + assert_equal face.description, human.polymorphic_face.description, "Description of face should be the same after changes to parent-owned instance" end def test_eager_loaded_child_instance_should_be_shared_with_parent_on_find - f = Face.all.merge!(where: { description: "confused" }, includes: :man).first - m = f.polymorphic_man - assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same before changes to child instance" - f.description = "gormless" - assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to child instance" - m.polymorphic_face.description = "pleasing" - assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to parent-owned instance" + face = Face.all.merge!(where: { description: "confused" }, includes: :human).first + human = face.polymorphic_human + assert_equal face.description, human.polymorphic_face.description, "Description of face should be the same before changes to child instance" + face.description = "gormless" + assert_equal face.description, human.polymorphic_face.description, "Description of face should be the same after changes to child instance" + human.polymorphic_face.description = "pleasing" + assert_equal face.description, human.polymorphic_face.description, "Description of face should be the same after changes to parent-owned instance" - f = Face.all.merge!(where: { description: "confused" }, includes: :man, order: "men.id").first - m = f.polymorphic_man - assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same before changes to child instance" - f.description = "gormless" - assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to child instance" - m.polymorphic_face.description = "pleasing" - assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to parent-owned instance" + face = Face.all.merge!(where: { description: "confused" }, includes: :human, order: "humans.id").first + human = face.polymorphic_human + assert_equal face.description, human.polymorphic_face.description, "Description of face should be the same before changes to child instance" + face.description = "gormless" + assert_equal face.description, human.polymorphic_face.description, "Description of face should be the same after changes to child instance" + human.polymorphic_face.description = "pleasing" + assert_equal face.description, human.polymorphic_face.description, "Description of face should be the same after changes to parent-owned instance" end def test_child_instance_should_be_shared_with_replaced_via_accessor_parent face = faces(:confused) - new_man = Man.new + new_human = Human.new - assert_not_nil face.polymorphic_man - face.polymorphic_man = new_man + assert_not_nil face.polymorphic_human + face.polymorphic_human = new_human - assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same before changes to parent instance" + assert_equal face.description, new_human.polymorphic_face.description, "Description of face should be the same before changes to parent instance" face.description = "Bongo" - assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to parent instance" - new_man.polymorphic_face.description = "Mungo" - assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to replaced-parent-owned instance" + assert_equal face.description, new_human.polymorphic_face.description, "Description of face should be the same after changes to parent instance" + new_human.polymorphic_face.description = "Mungo" + assert_equal face.description, new_human.polymorphic_face.description, "Description of face should be the same after changes to replaced-parent-owned instance" end def test_inversed_instance_should_not_be_reloaded_after_stale_state_changed - new_man = Man.new + new_human = Human.new face = Face.new - new_man.face = face + new_human.face = face - old_inversed_man = face.man - new_man.save! - new_inversed_man = face.man + old_inversed_human = face.human + new_human.save! + new_inversed_human = face.human - assert_same old_inversed_man, new_inversed_man + assert_same old_inversed_human, new_inversed_human end def test_inversed_instance_should_not_be_reloaded_after_stale_state_changed_with_validation - face = Face.new man: Man.new + face = Face.new human: Human.new - old_inversed_man = face.man + old_inversed_human = face.human face.save! - new_inversed_man = face.man + new_inversed_human = face.human - assert_same old_inversed_man, new_inversed_man + assert_same old_inversed_human, new_inversed_human end def test_should_not_try_to_set_inverse_instances_when_the_inverse_is_a_has_many - i = interests(:llama_wrangling) - m = i.polymorphic_man - assert_not_nil m.polymorphic_interests - iz = m.polymorphic_interests.detect { |_iz| _iz.id == i.id } + interest = interests(:llama_wrangling) + human = interest.polymorphic_human + assert_not_nil human.polymorphic_interests + iz = human.polymorphic_interests.detect { |_iz| _iz.id == interest.id } assert_not_nil iz - assert_equal i.topic, iz.topic, "Interest topics should be the same before changes to child" - i.topic = "Eating cheese with a spoon" - assert_not_equal i.topic, iz.topic, "Interest topics should not be the same after changes to child" + assert_equal interest.topic, iz.topic, "Interest topics should be the same before changes to child" + interest.topic = "Eating cheese with a spoon" + assert_not_equal interest.topic, iz.topic, "Interest topics should not be the same after changes to child" iz.topic = "Cow tipping" - assert_not_equal i.topic, iz.topic, "Interest topics should not be the same after changes to parent-owned instance" + assert_not_equal interest.topic, iz.topic, "Interest topics should not be the same after changes to parent-owned instance" end def test_with_has_many_inversing_should_try_to_set_inverse_instances_when_the_inverse_is_a_has_many with_has_many_inversing do - i = interests(:llama_wrangling) - m = i.polymorphic_man - assert_not_nil m.polymorphic_interests - iz = m.polymorphic_interests.detect { |_iz| _iz.id == i.id } + interest = interests(:llama_wrangling) + human = interest.polymorphic_human + assert_not_nil human.polymorphic_interests + iz = human.polymorphic_interests.detect { |_iz| _iz.id == interest.id } assert_not_nil iz - assert_equal i.topic, iz.topic, "Interest topics should be the same before changes to child" - i.topic = "Eating cheese with a spoon" - assert_equal i.topic, iz.topic, "Interest topics should be the same after changes to child" + assert_equal interest.topic, iz.topic, "Interest topics should be the same before changes to child" + interest.topic = "Eating cheese with a spoon" + assert_equal interest.topic, iz.topic, "Interest topics should be the same after changes to child" iz.topic = "Cow tipping" - assert_equal i.topic, iz.topic, "Interest topics should be the same after changes to parent-owned instance" + assert_equal interest.topic, iz.topic, "Interest topics should be the same after changes to parent-owned instance" end end def test_with_has_many_inversing_does_not_trigger_association_callbacks_on_set_when_the_inverse_is_a_has_many with_has_many_inversing do - man = interests(:llama_wrangling).polymorphic_man_with_callbacks - assert_not_predicate man, :add_callback_called? + human = interests(:llama_wrangling).polymorphic_human_with_callbacks + assert_not_predicate human, :add_callback_called? end end def test_trying_to_access_inverses_that_dont_exist_shouldnt_raise_an_error # Ideally this would, if only for symmetry's sake with other association types - assert_nothing_raised { Face.first.horrible_polymorphic_man } + assert_nothing_raised { Face.first.horrible_polymorphic_human } end def test_trying_to_set_polymorphic_inverses_that_dont_exist_at_all_should_raise_an_error - # fails because no class has the correct inverse_of for horrible_polymorphic_man - assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.horrible_polymorphic_man = Man.first } + # fails because no class has the correct inverse_of for horrible_polymorphic_human + assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.horrible_polymorphic_human = Human.first } end def test_trying_to_set_polymorphic_inverses_that_dont_exist_on_the_instance_being_set_should_raise_an_error - # passes because Man does have the correct inverse_of - assert_nothing_raised { Face.first.polymorphic_man = Man.first } + # passes because Human does have the correct inverse_of + assert_nothing_raised { Face.first.polymorphic_human = Human.first } # fails because Interest does have the correct inverse_of - assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.polymorphic_man = Interest.first } + assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.polymorphic_human = Interest.first } end end # NOTE - these tests might not be meaningful, ripped as they were from the parental_control plugin # which would guess the inverse rather than look for an explicit configuration option. class InverseMultipleHasManyInversesForSameModel < ActiveRecord::TestCase - fixtures :men, :interests, :zines + fixtures :humans, :interests, :zines def test_that_we_can_load_associations_that_have_the_same_reciprocal_name_from_different_models assert_nothing_raised do - i = Interest.first - i.zine - i.man + interest = Interest.first + interest.zine + interest.human end end def test_that_we_can_create_associations_that_have_the_same_reciprocal_name_from_different_models assert_nothing_raised do - i = Interest.first - i.build_zine(title: "Get Some in Winter! 2008") - i.build_man(name: "Gordon") - i.save! + interest = Interest.first + interest.build_zine(title: "Get Some in Winter! 2008") + interest.build_human(name: "Gordon") + interest.save! end end end diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index 70191b3c43..3623e292fc 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -19,7 +19,7 @@ require "models/ship" require "models/liquid" require "models/molecule" require "models/electron" -require "models/man" +require "models/human" require "models/interest" require "models/pirate" require "models/parrot" @@ -240,13 +240,13 @@ class AssociationProxyTest < ActiveRecord::TestCase end test "inverses get set of subsets of the association" do - man = Man.create - man.interests.create + human = Human.create + human.interests.create - man = Man.find(man.id) + human = Human.find(human.id) assert_queries(1) do - assert_equal man, man.interests.where("1=1").first.man + assert_equal human, human.interests.where("1=1").first.human end end @@ -354,8 +354,23 @@ class OverridingAssociationsTest < ActiveRecord::TestCase end end +class PreloaderTest < ActiveRecord::TestCase + fixtures :posts, :comments + + def test_preload_with_scope + post = posts(:welcome) + + preloader = ActiveRecord::Associations::Preloader.new + preloader.preload([post], :comments, Comment.where(body: "Thank you for the welcome")) + + assert_predicate post.comments, :loaded? + assert_equal [comments(:greetings)], post.comments + end +end + class GeneratedMethodsTest < ActiveRecord::TestCase fixtures :developers, :computers, :posts, :comments + def test_association_methods_override_attribute_methods_of_same_name assert_equal(developers(:david), computers(:workstation).developer) # this next line will fail if the attribute methods module is generated lazily diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 6f5e4062c7..1f3f2b28ad 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1719,7 +1719,7 @@ class BasicsTest < ActiveRecord::TestCase assert_match %r/\AWrite query attempted while in readonly mode: INSERT /, conn2_error.message ensure - ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler } + clean_up_connection_handler ActiveRecord::Base.establish_connection(:arunit) end end diff --git a/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb b/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb index 6f0d5fbb68..d184af3b7a 100644 --- a/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb @@ -21,7 +21,7 @@ module ActiveRecord end def teardown - ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler } + clean_up_connection_handler end class MultiConnectionTestModel < ActiveRecord::Base @@ -217,6 +217,14 @@ module ActiveRecord assert_equal "`connected_to` cannot accept a `database` argument with any other arguments.", error.message end + def test_database_argument_is_deprecated + assert_deprecated do + ActiveRecord::Base.connected_to(database: { writing: { adapter: "sqlite3", database: "test/db/primary.sqlite3" } }) { } + end + ensure + ActiveRecord::Base.establish_connection(:arunit) + end + def test_switching_connections_without_database_and_role_raises error = assert_raises(ArgumentError) do ActiveRecord::Base.connected_to { } @@ -369,8 +377,6 @@ module ActiveRecord end def test_connection_handlers_are_per_thread_and_not_per_fiber - original_handlers = ActiveRecord::Base.connection_handlers - ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler, reading: ActiveRecord::ConnectionAdapters::ConnectionHandler.new } reading_handler = ActiveRecord::Base.connection_handlers[:reading] @@ -382,7 +388,7 @@ module ActiveRecord assert_not_equal reading, ActiveRecord::Base.connection_handler assert_equal reading, reading_handler ensure - ActiveRecord::Base.connection_handlers = original_handlers + clean_up_connection_handler end def test_connection_handlers_swapping_connections_in_fiber diff --git a/activerecord/test/cases/connection_adapters/connection_handlers_multi_pool_config_test.rb b/activerecord/test/cases/connection_adapters/connection_handlers_multi_pool_config_test.rb index 18a593a2a5..5103796754 100644 --- a/activerecord/test/cases/connection_adapters/connection_handlers_multi_pool_config_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handlers_multi_pool_config_test.rb @@ -15,7 +15,7 @@ module ActiveRecord end def teardown - ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler } + clean_up_connection_handler end unless in_memory_db? @@ -31,10 +31,10 @@ module ActiveRecord @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config @writing_handler.establish_connection(:primary) - @writing_handler.establish_connection(:primary, :pool_config_two) + @writing_handler.establish_connection(:primary, shard: :pool_config_two) - default_pool = @writing_handler.retrieve_connection_pool("primary", :default) - other_pool = @writing_handler.retrieve_connection_pool("primary", :pool_config_two) + default_pool = @writing_handler.retrieve_connection_pool("primary", shard: :default) + other_pool = @writing_handler.retrieve_connection_pool("primary", shard: :pool_config_two) assert_not_nil default_pool assert_not_equal default_pool, other_pool @@ -59,13 +59,13 @@ module ActiveRecord @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config @writing_handler.establish_connection(:primary) - @writing_handler.establish_connection(:primary, :pool_config_two) + @writing_handler.establish_connection(:primary, shard: :pool_config_two) # remove default @writing_handler.remove_connection_pool("primary") assert_nil @writing_handler.retrieve_connection_pool("primary") - assert_not_nil @writing_handler.retrieve_connection_pool("primary", :pool_config_two) + assert_not_nil @writing_handler.retrieve_connection_pool("primary", shard: :pool_config_two) ensure ActiveRecord::Base.configurations = @prev_configs ActiveRecord::Base.establish_connection(:arunit) @@ -84,14 +84,14 @@ module ActiveRecord @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config @writing_handler.establish_connection(:primary) - @writing_handler.establish_connection(:primary, :pool_config_two) + @writing_handler.establish_connection(:primary, shard: :pool_config_two) # connect to default @writing_handler.connection_pool_list.first.checkout assert @writing_handler.connected?("primary") - assert @writing_handler.connected?("primary", :default) - assert_not @writing_handler.connected?("primary", :pool_config_two) + assert @writing_handler.connected?("primary", shard: :default) + assert_not @writing_handler.connected?("primary", shard: :pool_config_two) ensure ActiveRecord::Base.configurations = @prev_configs ActiveRecord::Base.establish_connection(:arunit) diff --git a/activerecord/test/cases/connection_adapters/connection_handlers_sharding_db_test.rb b/activerecord/test/cases/connection_adapters/connection_handlers_sharding_db_test.rb index 92beac0cfb..6d0e17320a 100644 --- a/activerecord/test/cases/connection_adapters/connection_handlers_sharding_db_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handlers_sharding_db_test.rb @@ -21,10 +21,20 @@ module ActiveRecord end def teardown - ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler } + clean_up_connection_handler end unless in_memory_db? + def test_establishing_a_connection_in_connected_to_block_uses_current_role_and_shard + ActiveRecord::Base.connected_to(shard: :shard_one) do + db_config = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary") + ActiveRecord::Base.establish_connection(db_config) + assert_nothing_raised { Person.first } + + assert_equal [:default, :shard_one], ActiveRecord::Base.connection_handlers[:writing].send(:owner_to_pool_manager).fetch("ActiveRecord::Base").instance_variable_get(:@name_to_pool_config).keys + end + end + def test_establish_connection_using_3_levels_config previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env" @@ -43,13 +53,13 @@ module ActiveRecord }) base_pool = ActiveRecord::Base.connection_handlers[:writing].retrieve_connection_pool("ActiveRecord::Base") - default_pool = ActiveRecord::Base.connection_handlers[:writing].retrieve_connection_pool("ActiveRecord::Base", :default) + default_pool = ActiveRecord::Base.connection_handlers[:writing].retrieve_connection_pool("ActiveRecord::Base", shard: :default) assert_equal base_pool, default_pool assert_equal "test/db/primary.sqlite3", default_pool.db_config.database assert_equal "primary", default_pool.db_config.name - assert_not_nil pool = ActiveRecord::Base.connection_handlers[:writing].retrieve_connection_pool("ActiveRecord::Base", :shard_one) + assert_not_nil pool = ActiveRecord::Base.connection_handlers[:writing].retrieve_connection_pool("ActiveRecord::Base", shard: :shard_one) assert_equal "test/db/primary_shard_one.sqlite3", pool.db_config.database assert_equal "primary_shard_one", pool.db_config.name ensure @@ -77,23 +87,23 @@ module ActiveRecord shard_one: { writing: :primary_shard_one, reading: :primary_shard_one_replica } }) - default_writing_pool = ActiveRecord::Base.connection_handlers[:writing].retrieve_connection_pool("ActiveRecord::Base", :default) + default_writing_pool = ActiveRecord::Base.connection_handlers[:writing].retrieve_connection_pool("ActiveRecord::Base", shard: :default) base_writing_pool = ActiveRecord::Base.connection_handlers[:writing].retrieve_connection_pool("ActiveRecord::Base") assert_equal base_writing_pool, default_writing_pool assert_equal "test/db/primary.sqlite3", default_writing_pool.db_config.database assert_equal "primary", default_writing_pool.db_config.name - default_reading_pool = ActiveRecord::Base.connection_handlers[:reading].retrieve_connection_pool("ActiveRecord::Base", :default) + default_reading_pool = ActiveRecord::Base.connection_handlers[:reading].retrieve_connection_pool("ActiveRecord::Base", shard: :default) base_reading_pool = ActiveRecord::Base.connection_handlers[:reading].retrieve_connection_pool("ActiveRecord::Base") assert_equal base_reading_pool, default_reading_pool assert_equal "test/db/primary.sqlite3", default_reading_pool.db_config.database assert_equal "primary_replica", default_reading_pool.db_config.name - assert_not_nil pool = ActiveRecord::Base.connection_handlers[:writing].retrieve_connection_pool("ActiveRecord::Base", :shard_one) + assert_not_nil pool = ActiveRecord::Base.connection_handlers[:writing].retrieve_connection_pool("ActiveRecord::Base", shard: :shard_one) assert_equal "test/db/primary_shard_one.sqlite3", pool.db_config.database assert_equal "primary_shard_one", pool.db_config.name - assert_not_nil pool = ActiveRecord::Base.connection_handlers[:reading].retrieve_connection_pool("ActiveRecord::Base", :shard_one) + assert_not_nil pool = ActiveRecord::Base.connection_handlers[:reading].retrieve_connection_pool("ActiveRecord::Base", shard: :shard_one) assert_equal "test/db/primary_shard_one.sqlite3", pool.db_config.database assert_equal "primary_shard_one_replica", pool.db_config.name ensure @@ -233,10 +243,10 @@ module ActiveRecord def test_retrieve_connection_pool_with_invalid_shard assert_not_nil @rw_handler.retrieve_connection_pool("ActiveRecord::Base") - assert_nil @rw_handler.retrieve_connection_pool("ActiveRecord::Base", :foo) + assert_nil @rw_handler.retrieve_connection_pool("ActiveRecord::Base", shard: :foo) assert_not_nil @ro_handler.retrieve_connection_pool("ActiveRecord::Base") - assert_nil @ro_handler.retrieve_connection_pool("ActiveRecord::Base", :foo) + assert_nil @ro_handler.retrieve_connection_pool("ActiveRecord::Base", shard: :foo) end def test_calling_connected_to_on_a_non_existent_shard_raises diff --git a/activerecord/test/cases/database_selector_test.rb b/activerecord/test/cases/database_selector_test.rb index 9742d34aef..0438adf7a3 100644 --- a/activerecord/test/cases/database_selector_test.rb +++ b/activerecord/test/cases/database_selector_test.rb @@ -12,7 +12,7 @@ module ActiveRecord end teardown do - ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler } + clean_up_connection_handler end def test_empty_session diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb index 27ec1a5a62..394ca511a4 100644 --- a/activerecord/test/cases/enum_test.rb +++ b/activerecord/test/cases/enum_test.rb @@ -236,6 +236,10 @@ class EnumTest < ActiveRecord::TestCase assert_nil @book.reload.status end + test "deserialize nil value to enum which defines nil value to hash" do + assert_equal "forgotten", books(:ddd).last_read + end + test "assign nil value" do @book.status = nil assert_nil @book.status diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 735c0dbdb1..c60684f1be 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -1375,7 +1375,14 @@ class FinderTest < ActiveRecord::TestCase limit: 3, order: "posts.id" ).to_a assert_equal 3, posts.size - assert_equal [0, 1, 1], posts.map(&:author_id).sort + assert_equal [1, 1, nil], posts.map(&:author_id) + end + + def test_custom_select_takes_precedence_over_original_value + posts = Post.select("UPPER(title) AS title") + assert_equal "WELCOME TO THE WEBLOG", posts.first.title + assert_equal "WELCOME TO THE WEBLOG", posts.preload(:comments).first.title + assert_equal "WELCOME TO THE WEBLOG", posts.eager_load(:comments).first.title end def test_eager_load_for_no_has_many_with_limit_and_joins_for_has_many diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index ed64d0bda2..7f7daf91e3 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -1399,7 +1399,6 @@ if current_adapter?(:SQLite3Adapter) && !in_memory_db? def setup @old_handler = ActiveRecord::Base.connection_handler - @old_handlers = ActiveRecord::Base.connection_handlers @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config db_config = ActiveRecord::DatabaseConfigurations::HashConfig.new(ENV["RAILS_ENV"], "readonly", readonly_config) @@ -1413,7 +1412,7 @@ if current_adapter?(:SQLite3Adapter) && !in_memory_db? def teardown ActiveRecord::Base.configurations = @prev_configs ActiveRecord::Base.connection_handler = @old_handler - ActiveRecord::Base.connection_handlers = @old_handlers + clean_up_connection_handler end def test_uses_writing_connection_for_fixtures diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 3c8209c103..4fe0523dda 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -149,6 +149,10 @@ def disable_extension!(extension, connection) connection.reconnect! end +def clean_up_connection_handler + ActiveRecord::Base.connection_handlers = { ActiveRecord::Base.writing_role => ActiveRecord::Base.default_connection_handler } +end + def load_schema # silence verbose schema loading original_stdout = $stdout diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 0d65dd7f9c..8946e0afb9 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -940,8 +940,10 @@ class MigrationTest < ActiveRecord::TestCase e = assert_raises(ActiveRecord::ConcurrentMigrationError) do silence_stream($stderr) do - migrator.send(:with_advisory_lock) do - ActiveRecord::AdvisoryLockBase.connection.release_advisory_lock(lock_id) + migrator.stub(:with_advisory_lock_connection, ->(&block) { block.call(ActiveRecord::Base.connection) }) do + migrator.send(:with_advisory_lock) do + ActiveRecord::Base.connection.release_advisory_lock(lock_id) + end end end end diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 7c16c4d20e..b0e30d6423 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -7,7 +7,7 @@ require "models/ship_part" require "models/bird" require "models/parrot" require "models/treasure" -require "models/man" +require "models/human" require "models/interest" require "models/owner" require "models/pet" @@ -140,19 +140,19 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase end def test_reject_if_with_a_proc_which_returns_true_always_for_has_many - Man.accepts_nested_attributes_for :interests, reject_if: proc { |attributes| true } - man = Man.create(name: "John") - interest = man.interests.create(topic: "photography") - man.update(interests_attributes: { topic: "gardening", id: interest.id }) + Human.accepts_nested_attributes_for :interests, reject_if: proc { |attributes| true } + human = Human.create(name: "John") + interest = human.interests.create(topic: "photography") + human.update(interests_attributes: { topic: "gardening", id: interest.id }) assert_equal "photography", interest.reload.topic end def test_destroy_works_independent_of_reject_if - Man.accepts_nested_attributes_for :interests, reject_if: proc { |attributes| true }, allow_destroy: true - man = Man.create(name: "Jon") - interest = man.interests.create(topic: "the ladies") - man.update(interests_attributes: { _destroy: "1", id: interest.id }) - assert_empty man.reload.interests + Human.accepts_nested_attributes_for :interests, reject_if: proc { |attributes| true }, allow_destroy: true + human = Human.create(name: "Jon") + interest = human.interests.create(topic: "the ladies") + human.update(interests_attributes: { _destroy: "1", id: interest.id }) + assert_empty human.reload.interests end def test_reject_if_is_not_short_circuited_if_allow_destroy_is_false @@ -169,10 +169,10 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase end def test_has_many_association_updating_a_single_record - Man.accepts_nested_attributes_for(:interests) - man = Man.create(name: "John") - interest = man.interests.create(topic: "photography") - man.update(interests_attributes: { topic: "gardening", id: interest.id }) + Human.accepts_nested_attributes_for(:interests) + human = Human.create(name: "John") + interest = human.interests.create(topic: "photography") + human.update(interests_attributes: { topic: "gardening", id: interest.id }) assert_equal "gardening", interest.reload.topic end @@ -186,12 +186,12 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase end def test_first_and_array_index_zero_methods_return_the_same_value_when_nested_attributes_are_set_to_update_existing_record - Man.accepts_nested_attributes_for(:interests) - man = Man.create(name: "John") - interest = man.interests.create topic: "gardening" - man = Man.find man.id - man.interests_attributes = [{ id: interest.id, topic: "gardening" }] - assert_equal man.interests.first.topic, man.interests[0].topic + Human.accepts_nested_attributes_for(:interests) + human = Human.create(name: "John") + interest = human.interests.create topic: "gardening" + human = Human.find human.id + human.interests_attributes = [{ id: interest.id, topic: "gardening" }] + assert_equal human.interests.first.topic, human.interests[0].topic end def test_allows_class_to_override_setter_and_call_super @@ -219,10 +219,10 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase end def test_should_not_create_duplicates_with_create_with - Man.accepts_nested_attributes_for(:interests) + Human.accepts_nested_attributes_for(:interests) assert_difference("Interest.count", 1) do - Man.create_with( + Human.create_with( interests_attributes: [{ topic: "Pirate king" }] ).find_or_create_by!( name: "Monkey D. Luffy" @@ -817,17 +817,17 @@ module NestedAttributesOnACollectionAssociationTests end def test_validate_presence_of_parent_works_with_inverse_of - Man.accepts_nested_attributes_for(:interests) - assert_equal :man, Man.reflect_on_association(:interests).options[:inverse_of] - assert_equal :interests, Interest.reflect_on_association(:man).options[:inverse_of] + Human.accepts_nested_attributes_for(:interests) + assert_equal :human, Human.reflect_on_association(:interests).options[:inverse_of] + assert_equal :interests, Interest.reflect_on_association(:human).options[:inverse_of] repair_validations(Interest) do - Interest.validates_presence_of(:man) - assert_difference "Man.count" do + Interest.validates_presence_of(:human) + assert_difference "Human.count" do assert_difference "Interest.count", 2 do - man = Man.create!(name: "John", + human = Human.create!(name: "John", interests_attributes: [{ topic: "Cars" }, { topic: "Sports" }]) - assert_equal 2, man.interests.count + assert_equal 2, human.interests.count end end end @@ -839,14 +839,14 @@ module NestedAttributesOnACollectionAssociationTests end def test_numeric_column_changes_from_zero_to_no_empty_string - Man.accepts_nested_attributes_for(:interests) + Human.accepts_nested_attributes_for(:interests) repair_validations(Interest) do Interest.validates_numericality_of(:zine_id) - man = Man.create(name: "John") - interest = man.interests.create(topic: "bar", zine_id: 0) + human = Human.create(name: "John") + interest = human.interests.create(topic: "bar", zine_id: 0) assert interest.save - assert_not man.update(interests_attributes: { id: interest.id, zine_id: "foo" }) + assert_not human.update(interests_attributes: { id: interest.id, zine_id: "foo" }) end end diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index 4d5e0adafe..46fbeefe80 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -93,7 +93,7 @@ class QueryCacheTest < ActiveRecord::TestCase mw.call({}) ensure - ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler } + clean_up_connection_handler end @@ -157,7 +157,7 @@ class QueryCacheTest < ActiveRecord::TestCase rd.close ensure - ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler } + clean_up_connection_handler end end @@ -445,17 +445,15 @@ class QueryCacheTest < ActiveRecord::TestCase def test_cache_is_available_when_using_a_not_connected_connection skip "In-Memory DB can't test for using a not connected connection" if in_memory_db? - with_temporary_connection_pool do - db_config = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary").dup - db_config.owner_name = "test2" - ActiveRecord::Base.connection_handler.establish_connection(db_config) - assert_not_predicate Task, :connected? + db_config = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary").dup + db_config.owner_name = "test2" + ActiveRecord::Base.connection_handler.establish_connection(db_config) + assert_not_predicate Task, :connected? - Task.cache do - assert_queries(1) { Task.find(1); Task.find(1) } - ensure - ActiveRecord::Base.connection_handler.remove_connection_pool(db_config.owner_name) - end + Task.cache do + assert_queries(1) { Task.find(1); Task.find(1) } + ensure + ActiveRecord::Base.connection_handler.remove_connection_pool(db_config.owner_name) end end @@ -532,44 +530,38 @@ class QueryCacheTest < ActiveRecord::TestCase end def test_query_cache_does_not_establish_connection_if_unconnected - with_temporary_connection_pool do - ActiveRecord::Base.clear_active_connections! - assert_not ActiveRecord::Base.connection_handler.active_connections? # sanity check + ActiveRecord::Base.clear_active_connections! + assert_not ActiveRecord::Base.connection_handler.active_connections? # sanity check - middleware { - assert_not ActiveRecord::Base.connection_handler.active_connections?, "QueryCache forced ActiveRecord::Base to establish a connection in setup" - }.call({}) + middleware { + assert_not ActiveRecord::Base.connection_handler.active_connections?, "QueryCache forced ActiveRecord::Base to establish a connection in setup" + }.call({}) - assert_not ActiveRecord::Base.connection_handler.active_connections?, "QueryCache forced ActiveRecord::Base to establish a connection in cleanup" - end + assert_not ActiveRecord::Base.connection_handler.active_connections?, "QueryCache forced ActiveRecord::Base to establish a connection in cleanup" end def test_query_cache_is_enabled_on_connections_established_after_middleware_runs - with_temporary_connection_pool do - ActiveRecord::Base.clear_active_connections! - assert_not ActiveRecord::Base.connection_handler.active_connections? # sanity check + ActiveRecord::Base.clear_active_connections! + assert_not ActiveRecord::Base.connection_handler.active_connections? # sanity check - middleware { - assert_predicate ActiveRecord::Base.connection, :query_cache_enabled - }.call({}) - assert_not_predicate ActiveRecord::Base.connection, :query_cache_enabled - end + middleware { + assert_predicate ActiveRecord::Base.connection, :query_cache_enabled + }.call({}) + assert_not_predicate ActiveRecord::Base.connection, :query_cache_enabled end def test_query_caching_is_local_to_the_current_thread - with_temporary_connection_pool do - ActiveRecord::Base.clear_active_connections! + ActiveRecord::Base.clear_active_connections! - middleware { - assert ActiveRecord::Base.connection_pool.query_cache_enabled - assert ActiveRecord::Base.connection.query_cache_enabled + middleware { + assert ActiveRecord::Base.connection_pool.query_cache_enabled + assert ActiveRecord::Base.connection.query_cache_enabled - Thread.new { - assert_not ActiveRecord::Base.connection_pool.query_cache_enabled - assert_not ActiveRecord::Base.connection.query_cache_enabled - }.join - }.call({}) - end + Thread.new { + assert_not ActiveRecord::Base.connection_pool.query_cache_enabled + assert_not ActiveRecord::Base.connection.query_cache_enabled + }.join + }.call({}) end def test_query_cache_is_enabled_on_all_connection_pools @@ -583,41 +575,39 @@ class QueryCacheTest < ActiveRecord::TestCase def test_clear_query_cache_is_called_on_all_connections skip "with in memory db, reading role won't be able to see database on writing role" if in_memory_db? - with_temporary_connection_pool do - ActiveRecord::Base.connection_handlers = { - writing: ActiveRecord::Base.default_connection_handler, - reading: ActiveRecord::ConnectionAdapters::ConnectionHandler.new - } + ActiveRecord::Base.connection_handlers = { + writing: ActiveRecord::Base.default_connection_handler, + reading: ActiveRecord::ConnectionAdapters::ConnectionHandler.new + } + ActiveRecord::Base.connected_to(role: :reading) do + db_config = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary") + ActiveRecord::Base.establish_connection(db_config) + end + + mw = middleware { |env| ActiveRecord::Base.connected_to(role: :reading) do - db_config = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary") - ActiveRecord::Base.establish_connection(db_config) + @topic = Topic.first end - mw = middleware { |env| - ActiveRecord::Base.connected_to(role: :reading) do - @topic = Topic.first - end + assert @topic - assert @topic + ActiveRecord::Base.connected_to(role: :writing) do + @topic.title = "It doesn't have to be crazy at work" + @topic.save! + end - ActiveRecord::Base.connected_to(role: :writing) do - @topic.title = "It doesn't have to be crazy at work" - @topic.save! - end + assert_equal "It doesn't have to be crazy at work", @topic.title + ActiveRecord::Base.connected_to(role: :reading) do + @topic = Topic.first assert_equal "It doesn't have to be crazy at work", @topic.title + end + } - ActiveRecord::Base.connected_to(role: :reading) do - @topic = Topic.first - assert_equal "It doesn't have to be crazy at work", @topic.title - end - } - - mw.call({}) - end + mw.call({}) ensure - ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler } + clean_up_connection_handler end test "query cache is enabled in threads with shared connection" do diff --git a/activerecord/test/cases/relation/select_test.rb b/activerecord/test/cases/relation/select_test.rb index 092d3c4d5b..7c98df8424 100644 --- a/activerecord/test/cases/relation/select_test.rb +++ b/activerecord/test/cases/relation/select_test.rb @@ -25,6 +25,22 @@ module ActiveRecord assert_equal expected, actual end + def test_non_select_columns_wont_be_loaded + posts = Post.select("UPPER(title) AS title") + + assert_non_select_columns_wont_be_loaded(posts.first) + assert_non_select_columns_wont_be_loaded(posts.preload(:comments).first) + assert_non_select_columns_wont_be_loaded(posts.eager_load(:comments).first) + end + + def assert_non_select_columns_wont_be_loaded(post) + assert_equal "WELCOME TO THE WEBLOG", post.title + assert_raise(ActiveModel::MissingAttributeError) do + post.body + end + end + private :assert_non_select_columns_wont_be_loaded + def test_type_casted_extra_select_with_eager_loading posts = Post.select("posts.id * 1.1 AS foo").eager_load(:comments) assert_equal 1.1, posts.first.foo @@ -32,18 +48,12 @@ module ActiveRecord def test_aliased_select_using_as_with_joins_and_includes posts = Post.select("posts.id AS field_alias").joins(:comments).includes(:comments) - assert_equal %w( - id author_id title body type legacy_comments_count taggings_with_delete_all_count taggings_with_destroy_count - tags_count indestructible_tags_count tags_with_destroy_count tags_with_nullify_count field_alias - ), posts.first.attributes.keys + assert_equal %w(id field_alias), posts.first.attributes.keys end def test_aliased_select_not_using_as_with_joins_and_includes posts = Post.select("posts.id field_alias").joins(:comments).includes(:comments) - assert_equal %w( - id author_id title body type legacy_comments_count taggings_with_delete_all_count taggings_with_destroy_count - tags_count indestructible_tags_count tags_with_destroy_count tags_with_nullify_count field_alias - ), posts.first.attributes.keys + assert_equal %w(id field_alias), posts.first.attributes.keys end def test_star_select_with_joins_and_includes diff --git a/activerecord/test/cases/relation/where_chain_test.rb b/activerecord/test/cases/relation/where_chain_test.rb index 2528a89d4f..94bc06362d 100644 --- a/activerecord/test/cases/relation/where_chain_test.rb +++ b/activerecord/test/cases/relation/where_chain_test.rb @@ -3,7 +3,7 @@ require "cases/helper" require "models/post" require "models/author" -require "models/man" +require "models/human" require "models/essay" require "models/comment" require "models/categorization" @@ -11,7 +11,7 @@ require "models/categorization" module ActiveRecord class WhereChainTest < ActiveRecord::TestCase - fixtures :posts, :comments, :authors, :men, :essays + fixtures :posts, :comments, :authors, :humans, :essays def test_missing_with_association assert posts(:authorless).author.blank? @@ -105,8 +105,8 @@ module ActiveRecord end def test_rewhere_with_polymorphic_association - relation = Essay.where(writer: authors(:david)).rewhere(writer: men(:steve)) - expected = Essay.where(writer: men(:steve)) + relation = Essay.where(writer: authors(:david)).rewhere(writer: humans(:steve)) + expected = Essay.where(writer: humans(:steve)) assert_equal expected.to_a, relation.to_a end diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb index a1acf19565..2c1504179b 100644 --- a/activerecord/test/cases/tasks/database_tasks_test.rb +++ b/activerecord/test/cases/tasks/database_tasks_test.rb @@ -1112,7 +1112,7 @@ module ActiveRecord def teardown SchemaMigration.delete_all InternalMetadata.delete_all - ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler } + clean_up_connection_handler end def test_truncate_tables diff --git a/activerecord/test/cases/test_fixtures_test.rb b/activerecord/test/cases/test_fixtures_test.rb index ca65d5b133..cba6107b9a 100644 --- a/activerecord/test/cases/test_fixtures_test.rb +++ b/activerecord/test/cases/test_fixtures_test.rb @@ -43,7 +43,6 @@ class TestFixturesTest < ActiveRecord::TestCase end end - old_handlers = ActiveRecord::Base.connection_handlers old_handler = ActiveRecord::Base.connection_handler ActiveRecord::Base.connection_handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new ActiveRecord::Base.connection_handlers = {} @@ -53,7 +52,7 @@ class TestFixturesTest < ActiveRecord::TestCase assert_predicate(test_result, :passed?) ensure ActiveRecord::Base.connection_handler = old_handler - ActiveRecord::Base.connection_handlers = old_handlers + clean_up_connection_handler FileUtils.rm_r(tmp_dir) end end diff --git a/activerecord/test/cases/validations/absence_validation_test.rb b/activerecord/test/cases/validations/absence_validation_test.rb index 1982734f02..a4eb42c4d3 100644 --- a/activerecord/test/cases/validations/absence_validation_test.rb +++ b/activerecord/test/cases/validations/absence_validation_test.rb @@ -3,12 +3,12 @@ require "cases/helper" require "models/face" require "models/interest" -require "models/man" +require "models/human" require "models/topic" class AbsenceValidationTest < ActiveRecord::TestCase def test_non_association - boy_klass = Class.new(Man) do + boy_klass = Class.new(Human) do def self.name; "Boy" end validates_absence_of :name end @@ -18,7 +18,7 @@ class AbsenceValidationTest < ActiveRecord::TestCase end def test_has_one_marked_for_destruction - boy_klass = Class.new(Man) do + boy_klass = Class.new(Human) do def self.name; "Boy" end validates_absence_of :face end @@ -32,7 +32,7 @@ class AbsenceValidationTest < ActiveRecord::TestCase end def test_has_many_marked_for_destruction - boy_klass = Class.new(Man) do + boy_klass = Class.new(Human) do def self.name; "Boy" end validates_absence_of :interests end @@ -48,7 +48,7 @@ class AbsenceValidationTest < ActiveRecord::TestCase end def test_does_not_call_to_a_on_associations - boy_klass = Class.new(Man) do + boy_klass = Class.new(Human) do def self.name; "Boy" end validates_absence_of :face end diff --git a/activerecord/test/cases/validations/association_validation_test.rb b/activerecord/test/cases/validations/association_validation_test.rb index ce6d42b34b..5245ca744a 100644 --- a/activerecord/test/cases/validations/association_validation_test.rb +++ b/activerecord/test/cases/validations/association_validation_test.rb @@ -3,7 +3,7 @@ require "cases/helper" require "models/topic" require "models/reply" -require "models/man" +require "models/human" require "models/interest" class AssociationValidationTest < ActiveRecord::TestCase @@ -80,20 +80,20 @@ class AssociationValidationTest < ActiveRecord::TestCase def test_validates_presence_of_belongs_to_association__parent_is_new_record repair_validations(Interest) do - # Note that Interest and Man have the :inverse_of option set - Interest.validates_presence_of(:man) - man = Man.new(name: "John") - interest = man.interests.build(topic: "Airplanes") - assert interest.valid?, "Expected interest to be valid, but was not. Interest should have a man object associated" + # Note that Interest and Human have the :inverse_of option set + Interest.validates_presence_of(:human) + human = Human.new(name: "John") + interest = human.interests.build(topic: "Airplanes") + assert interest.valid?, "Expected interest to be valid, but was not. Interest should have a human object associated" end end def test_validates_presence_of_belongs_to_association__existing_parent repair_validations(Interest) do - Interest.validates_presence_of(:man) - man = Man.create!(name: "John") - interest = man.interests.build(topic: "Airplanes") - assert interest.valid?, "Expected interest to be valid, but was not. Interest should have a man object associated" + Interest.validates_presence_of(:human) + human = Human.create!(name: "John") + interest = human.interests.build(topic: "Airplanes") + assert interest.valid?, "Expected interest to be valid, but was not. Interest should have a human object associated" end end end diff --git a/activerecord/test/cases/validations/presence_validation_test.rb b/activerecord/test/cases/validations/presence_validation_test.rb index 4b9cbe9098..4230832f98 100644 --- a/activerecord/test/cases/validations/presence_validation_test.rb +++ b/activerecord/test/cases/validations/presence_validation_test.rb @@ -1,14 +1,14 @@ # frozen_string_literal: true require "cases/helper" -require "models/man" +require "models/human" require "models/face" require "models/interest" require "models/speedometer" require "models/dashboard" class PresenceValidationTest < ActiveRecord::TestCase - class Boy < Man; end + class Boy < Human; end repair_validations(Boy) diff --git a/activerecord/test/fixtures/essays.yml b/activerecord/test/fixtures/essays.yml index 9fd07aded3..be96dcacb3 100644 --- a/activerecord/test/fixtures/essays.yml +++ b/activerecord/test/fixtures/essays.yml @@ -12,5 +12,5 @@ mary_stay_home: steve_connecting_the_dots: name: Connecting The Dots - writer_type: Man + writer_type: Human writer_id: Steve diff --git a/activerecord/test/fixtures/faces.yml b/activerecord/test/fixtures/faces.yml index c8e4a34484..b034f59c6a 100644 --- a/activerecord/test/fixtures/faces.yml +++ b/activerecord/test/fixtures/faces.yml @@ -1,11 +1,11 @@ trusting: description: trusting - man: gordon + human: gordon weather_beaten: description: weather beaten - man: steve + human: steve confused: description: confused - polymorphic_man: gordon (Man) + polymorphic_human: gordon (Human) diff --git a/activerecord/test/fixtures/men.yml b/activerecord/test/fixtures/humans.yml similarity index 100% rename from activerecord/test/fixtures/men.yml rename to activerecord/test/fixtures/humans.yml diff --git a/activerecord/test/fixtures/interests.yml b/activerecord/test/fixtures/interests.yml index 9200a19d5a..335e855dbc 100644 --- a/activerecord/test/fixtures/interests.yml +++ b/activerecord/test/fixtures/interests.yml @@ -1,33 +1,33 @@ trainspotting: topic: Trainspotting zine: staying_in - man: gordon + human: gordon birdwatching: topic: Birdwatching zine: staying_in - man: gordon + human: gordon stamp_collecting: topic: Stamp Collecting zine: staying_in - man: gordon + human: gordon hunting: topic: Hunting zine: going_out - man: steve + human: steve woodsmanship: topic: Woodsmanship zine: going_out - man: steve + human: steve survival: topic: Survival zine: going_out - man: steve + human: steve llama_wrangling: topic: Llama Wrangling - polymorphic_man: gordon (Man) + polymorphic_human: gordon (Human) diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index 373f6d2af6..a19890a0a5 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -93,6 +93,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 :general_categorizations, -> { joins(:category).where("categories.name": "General") }, class_name: "Categorization" + has_many :general_posts, through: :general_categorizations, source: :post + 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 diff --git a/activerecord/test/models/face.rb b/activerecord/test/models/face.rb index 45ccc442ba..178834155f 100644 --- a/activerecord/test/models/face.rb +++ b/activerecord/test/models/face.rb @@ -1,16 +1,16 @@ # frozen_string_literal: true class Face < ActiveRecord::Base - belongs_to :man, inverse_of: :face - belongs_to :human, polymorphic: true - belongs_to :polymorphic_man, polymorphic: true, inverse_of: :polymorphic_face + belongs_to :human, inverse_of: :face + belongs_to :super_human, polymorphic: true + belongs_to :polymorphic_human, polymorphic: true, inverse_of: :polymorphic_face # Oracle identifier length is limited to 30 bytes or less, `polymorphic` renamed `poly` - belongs_to :poly_man_without_inverse, polymorphic: true + belongs_to :poly_human_without_inverse, polymorphic: true # These are "broken" inverse_of associations for the purposes of testing - belongs_to :horrible_man, class_name: "Man", inverse_of: :horrible_face - belongs_to :horrible_polymorphic_man, polymorphic: true, inverse_of: :horrible_polymorphic_face + belongs_to :horrible_human, class_name: "Human", inverse_of: :horrible_face + belongs_to :horrible_polymorphic_human, polymorphic: true, inverse_of: :horrible_polymorphic_face validate do - man + human end end diff --git a/activerecord/test/models/man.rb b/activerecord/test/models/human.rb similarity index 61% rename from activerecord/test/models/man.rb rename to activerecord/test/models/human.rb index 118a4f27cd..d19b08e25a 100644 --- a/activerecord/test/models/man.rb +++ b/activerecord/test/models/human.rb @@ -1,28 +1,30 @@ # frozen_string_literal: true -class Man < ActiveRecord::Base - has_one :face, inverse_of: :man - has_one :polymorphic_face, class_name: "Face", as: :polymorphic_man, inverse_of: :polymorphic_man - has_one :polymorphic_face_without_inverse, class_name: "Face", as: :poly_man_without_inverse - has_many :interests, inverse_of: :man +class Human < ActiveRecord::Base + self.table_name = "humans" + + has_one :face, inverse_of: :human + has_one :polymorphic_face, class_name: "Face", as: :polymorphic_human, inverse_of: :polymorphic_human + has_one :polymorphic_face_without_inverse, class_name: "Face", as: :poly_human_without_inverse + has_many :interests, inverse_of: :human has_many :interests_with_callbacks, class_name: "Interest", before_add: :add_called, after_add: :add_called, - inverse_of: :man_with_callbacks + inverse_of: :human_with_callbacks has_many :polymorphic_interests, class_name: "Interest", - as: :polymorphic_man, - inverse_of: :polymorphic_man + as: :polymorphic_human, + inverse_of: :polymorphic_human has_many :polymorphic_interests_with_callbacks, class_name: "Interest", - as: :polymorphic_man, + as: :polymorphic_human, before_add: :add_called, after_add: :add_called, - inverse_of: :polymorphic_man + inverse_of: :polymorphic_human # These are "broken" inverse_of associations for the purposes of testing - has_one :dirty_face, class_name: "Face", inverse_of: :dirty_man - has_many :secret_interests, class_name: "Interest", inverse_of: :secret_man + has_one :dirty_face, class_name: "Face", inverse_of: :dirty_human + has_many :secret_interests, class_name: "Interest", inverse_of: :secret_human has_one :mixed_case_monkey attribute :add_callback_called, :boolean, default: false @@ -32,5 +34,5 @@ class Man < ActiveRecord::Base end end -class Human < Man +class SuperHuman < Human end diff --git a/activerecord/test/models/interest.rb b/activerecord/test/models/interest.rb index d2da6a46ef..0c0cef190f 100644 --- a/activerecord/test/models/interest.rb +++ b/activerecord/test/models/interest.rb @@ -1,15 +1,15 @@ # frozen_string_literal: true class Interest < ActiveRecord::Base - belongs_to :man, inverse_of: :interests - belongs_to :man_with_callbacks, - class_name: "Man", - foreign_key: :man_id, + belongs_to :human, inverse_of: :interests + belongs_to :human_with_callbacks, + class_name: "Human", + foreign_key: :human_id, inverse_of: :interests_with_callbacks - belongs_to :polymorphic_man, polymorphic: true, inverse_of: :polymorphic_interests - belongs_to :polymorphic_man_with_callbacks, - foreign_key: :polymorphic_man_id, - foreign_type: :polymorphic_man_type, + belongs_to :polymorphic_human, polymorphic: true, inverse_of: :polymorphic_interests + belongs_to :polymorphic_human_with_callbacks, + foreign_key: :polymorphic_human_id, + foreign_type: :polymorphic_human_type, polymorphic: true, inverse_of: :polymorphic_interests_with_callbacks belongs_to :zine, inverse_of: :interests diff --git a/activerecord/test/models/mixed_case_monkey.rb b/activerecord/test/models/mixed_case_monkey.rb index 8e92f68817..f4985ebadb 100644 --- a/activerecord/test/models/mixed_case_monkey.rb +++ b/activerecord/test/models/mixed_case_monkey.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true class MixedCaseMonkey < ActiveRecord::Base - belongs_to :man + belongs_to :human end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 7bb316fdb4..a0b4442465 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -1009,27 +1009,27 @@ ActiveRecord::Schema.define do create_table(t, force: true) { } end - create_table :men, force: true do |t| + create_table :humans, force: true do |t| t.string :name end create_table :faces, force: true do |t| t.string :description - t.integer :man_id - t.integer :polymorphic_man_id - t.string :polymorphic_man_type - t.integer :poly_man_without_inverse_id - t.string :poly_man_without_inverse_type - t.integer :horrible_polymorphic_man_id - t.string :horrible_polymorphic_man_type - t.references :human, polymorphic: true, index: false + t.integer :human_id + t.integer :polymorphic_human_id + t.string :polymorphic_human_type + t.integer :poly_human_without_inverse_id + t.string :poly_human_without_inverse_type + t.integer :horrible_polymorphic_human_id + t.string :horrible_polymorphic_human_type + t.references :super_human, polymorphic: true, index: false end create_table :interests, force: true do |t| t.string :topic - t.integer :man_id - t.integer :polymorphic_man_id - t.string :polymorphic_man_type + t.integer :human_id + t.integer :polymorphic_human_id + t.string :polymorphic_human_type t.integer :zine_id end diff --git a/activestorage/app/models/active_storage/attachment.rb b/activestorage/app/models/active_storage/attachment.rb index f7a8ff8922..9b626be82d 100644 --- a/activestorage/app/models/active_storage/attachment.rb +++ b/activestorage/app/models/active_storage/attachment.rb @@ -11,12 +11,12 @@ class ActiveStorage::Attachment < ActiveRecord::Base self.table_name = "active_storage_attachments" belongs_to :record, polymorphic: true, touch: true - belongs_to :blob, class_name: "ActiveStorage::Blob" + belongs_to :blob, class_name: "ActiveStorage::Blob", autosave: true delegate_missing_to :blob delegate :signed_id, to: :blob - after_create_commit :mirror_blob_later, :analyze_blob_later, :identify_blob + after_create_commit :mirror_blob_later, :analyze_blob_later after_destroy_commit :purge_dependent_blob_later # Synchronously deletes the attachment and {purges the blob}[rdoc-ref:ActiveStorage::Blob#purge]. @@ -38,10 +38,6 @@ class ActiveStorage::Attachment < ActiveRecord::Base end private - def identify_blob - blob.identify - end - def analyze_blob_later blob.analyze_later unless blob.analyzed? end diff --git a/activestorage/app/models/active_storage/blob.rb b/activestorage/app/models/active_storage/blob.rb index 163f633502..414309b07c 100644 --- a/activestorage/app/models/active_storage/blob.rb +++ b/activestorage/app/models/active_storage/blob.rb @@ -53,6 +53,8 @@ class ActiveStorage::Blob < ActiveRecord::Base self.service_name ||= self.class.service.name end + after_update_commit :update_service_metadata, if: :content_type_previously_changed? + before_destroy(prepend: true) do raise ActiveRecord::InvalidForeignKey if attachments.exists? end @@ -326,6 +328,10 @@ class ActiveStorage::Blob < ActiveRecord::Base { content_type: content_type } end end + + def update_service_metadata + service.update_metadata key, **service_metadata if service_metadata.any? + end end ActiveSupport.run_load_hooks :active_storage_blob, ActiveStorage::Blob diff --git a/activestorage/app/models/active_storage/blob/identifiable.rb b/activestorage/app/models/active_storage/blob/identifiable.rb index c8d9569c5d..f2a07a634d 100644 --- a/activestorage/app/models/active_storage/blob/identifiable.rb +++ b/activestorage/app/models/active_storage/blob/identifiable.rb @@ -2,9 +2,14 @@ module ActiveStorage::Blob::Identifiable def identify + identify_without_saving + save! + end + + def identify_without_saving unless identified? - update! content_type: identify_content_type, identified: true - update_service_metadata + self.content_type = identify_content_type + self.identified = true end end @@ -24,8 +29,4 @@ module ActiveStorage::Blob::Identifiable "" end end - - def update_service_metadata - service.update_metadata key, **service_metadata if service_metadata.any? - end end diff --git a/activestorage/lib/active_storage/attached/changes/create_many.rb b/activestorage/lib/active_storage/attached/changes/create_many.rb index a7a8553e0f..4afc3d3831 100644 --- a/activestorage/lib/active_storage/attached/changes/create_many.rb +++ b/activestorage/lib/active_storage/attached/changes/create_many.rb @@ -6,6 +6,7 @@ module ActiveStorage def initialize(name, record, attachables) @name, @record, @attachables = name, record, Array(attachables) + blobs.each(&:identify_without_saving) end def attachments diff --git a/activestorage/lib/active_storage/attached/changes/create_one.rb b/activestorage/lib/active_storage/attached/changes/create_one.rb index b61d3b078c..c22b147d1e 100644 --- a/activestorage/lib/active_storage/attached/changes/create_one.rb +++ b/activestorage/lib/active_storage/attached/changes/create_one.rb @@ -9,6 +9,7 @@ module ActiveStorage def initialize(name, record, attachable) @name, @record, @attachable = name, record, attachable + blob.identify_without_saving end def attachment @@ -65,7 +66,7 @@ module ActiveStorage **attachable.reverse_merge( record: record, service_name: attachment_service_name - ) + ).symbolize_keys ) when String ActiveStorage::Blob.find_signed!(attachable, record: record) diff --git a/activestorage/lib/active_storage/attached/many.rb b/activestorage/lib/active_storage/attached/many.rb index 0a876a0065..913b534f52 100644 --- a/activestorage/lib/active_storage/attached/many.rb +++ b/activestorage/lib/active_storage/attached/many.rb @@ -29,7 +29,8 @@ module ActiveStorage # document.images.attach([ first_blob, second_blob ]) def attach(*attachables) if record.persisted? && !record.changed? - record.update(name => blobs + attachables.flatten) + record.public_send("#{name}=", blobs + attachables.flatten) + record.save else record.public_send("#{name}=", (change&.attachables || blobs) + attachables.flatten) end diff --git a/activestorage/lib/active_storage/attached/one.rb b/activestorage/lib/active_storage/attached/one.rb index 2b122b5ba2..2e8b2a91cf 100644 --- a/activestorage/lib/active_storage/attached/one.rb +++ b/activestorage/lib/active_storage/attached/one.rb @@ -29,7 +29,8 @@ module ActiveStorage # person.avatar.attach(avatar_blob) # ActiveStorage::Blob object def attach(attachable) if record.persisted? && !record.changed? - record.update(name => attachable) + record.public_send("#{name}=", attachable) + record.save else record.public_send("#{name}=", attachable) end diff --git a/activestorage/test/models/attached/one_test.rb b/activestorage/test/models/attached/one_test.rb index c03102fcb1..dbdf547995 100644 --- a/activestorage/test/models/attached/one_test.rb +++ b/activestorage/test/models/attached/one_test.rb @@ -319,6 +319,13 @@ class ActiveStorage::OneAttachedTest < ActiveSupport::TestCase assert_equal 2736, @user.avatar.metadata[:height] end + test "creating an attachment as part of an autosave association through nested attributes" do + group = Group.create!(users_attributes: [{ name: "John", avatar: { io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" } }]) + group.save! + new_user = User.find_by(name: "John") + assert new_user.avatar.attached? + end + test "updating an attachment as part of an autosave association" do group = Group.create!(users: [@user]) @user.avatar = fixture_file_upload("racecar.jpg") diff --git a/activestorage/test/models/attachment_test.rb b/activestorage/test/models/attachment_test.rb index ebcb837e86..3cd9ee3a07 100644 --- a/activestorage/test/models/attachment_test.rb +++ b/activestorage/test/models/attachment_test.rb @@ -2,6 +2,7 @@ require "test_helper" require "database/setup" +require "active_support/testing/method_call_assertions" class ActiveStorage::AttachmentTest < ActiveSupport::TestCase include ActiveJob::TestHelper @@ -50,6 +51,38 @@ class ActiveStorage::AttachmentTest < ActiveSupport::TestCase end end + test "directly-uploaded blob identification for one attached occurs before validation" do + blob = directly_upload_file_blob(filename: "racecar.jpg", content_type: "application/octet-stream") + + assert_blob_identified_before_owner_validated(@user, blob, "image/jpeg") do + @user.avatar.attach(blob) + end + end + + test "directly-uploaded blob identification for many attached occurs before validation" do + blob = directly_upload_file_blob(filename: "racecar.jpg", content_type: "application/octet-stream") + + assert_blob_identified_before_owner_validated(@user, blob, "image/jpeg") do + @user.highlights.attach(blob) + end + end + + test "directly-uploaded blob identification for one attached occurs outside transaction" do + blob = directly_upload_file_blob(filename: "racecar.jpg") + + assert_blob_identified_outside_transaction(blob) do + @user.avatar.attach(blob) + end + end + + test "directly-uploaded blob identification for many attached occurs outside transaction" do + blob = directly_upload_file_blob(filename: "racecar.jpg") + + assert_blob_identified_outside_transaction(blob) do + @user.highlights.attach(blob) + end + end + test "getting a signed blob ID from an attachment" do blob = create_blob @user.avatar.attach(blob) @@ -65,4 +98,33 @@ class ActiveStorage::AttachmentTest < ActiveSupport::TestCase signed_id_generated_old_way = ActiveStorage.verifier.generate(@user.avatar.id, purpose: :blob_id) assert_equal blob, ActiveStorage::Blob.find_signed!(signed_id_generated_old_way) end + + private + def assert_blob_identified_before_owner_validated(owner, blob, content_type) + validated_content_type = nil + + owner.class.validate do + validated_content_type ||= blob.content_type + end + + yield + + assert_equal content_type, validated_content_type + assert_equal content_type, blob.reload.content_type + end + + def assert_blob_identified_outside_transaction(blob) + baseline_transaction_depth = ActiveRecord::Base.connection.open_transactions + max_transaction_depth = -1 + + track_transaction_depth = ->(*) do + max_transaction_depth = [ActiveRecord::Base.connection.open_transactions, max_transaction_depth].max + end + + blob.stub(:identify_without_saving, track_transaction_depth) do + yield + end + + assert_equal 0, (max_transaction_depth - baseline_transaction_depth) + end end diff --git a/activestorage/test/models/blob_test.rb b/activestorage/test/models/blob_test.rb index 532896cb27..10a4c69478 100644 --- a/activestorage/test/models/blob_test.rb +++ b/activestorage/test/models/blob_test.rb @@ -260,6 +260,16 @@ class ActiveStorage::BlobTest < ActiveSupport::TestCase assert_equal ["is invalid"], blob.errors[:service_name] end + test "updating the content_type updates service metadata" do + blob = directly_upload_file_blob(filename: "racecar.jpg", content_type: "application/octet-stream") + + expected_arguments = [blob.key, content_type: "image/jpeg"] + + assert_called_with(blob.service, :update_metadata, expected_arguments) do + blob.update!(content_type: "image/jpeg") + end + end + private def expected_url_for(blob, disposition: :attachment, filename: nil, content_type: nil, service_name: :local) filename ||= blob.filename diff --git a/activestorage/test/models/variant_test.rb b/activestorage/test/models/variant_test.rb index 63c74f48ea..c4779fdf8d 100644 --- a/activestorage/test/models/variant_test.rb +++ b/activestorage/test/models/variant_test.rb @@ -162,12 +162,12 @@ class ActiveStorage::VariantTest < ActiveSupport::TestCase end test "resized variation of BMP blob" do - blob = create_file_blob(filename: "colors.bmp") + blob = create_file_blob(filename: "colors.bmp", content_type: "image/bmp") variant = blob.variant(resize: "15x15").processed - assert_match(/colors\.bmp/, variant.url) + assert_match(/colors\.png/, variant.url) image = read_image(variant) - assert_equal "BMP", image.type + assert_equal "PNG", image.type assert_equal 15, image.width assert_equal 8, image.height end diff --git a/activestorage/test/test_helper.rb b/activestorage/test/test_helper.rb index 58dc21950b..c83f47b571 100644 --- a/activestorage/test/test_helper.rb +++ b/activestorage/test/test_helper.rb @@ -127,6 +127,8 @@ end class Group < ActiveRecord::Base has_one_attached :avatar has_many :users, autosave: true + + accepts_nested_attributes_for :users end require_relative "../../tools/test_common" diff --git a/activesupport/lib/active_support/core_ext/enumerable.rb b/activesupport/lib/active_support/core_ext/enumerable.rb index 6ba68ababa..97c918a71f 100644 --- a/activesupport/lib/active_support/core_ext/enumerable.rb +++ b/activesupport/lib/active_support/core_ext/enumerable.rb @@ -153,7 +153,8 @@ module Enumerable if keys.many? map { |element| keys.map { |key| element[key] } } else - map { |element| element[keys.first] } + key = keys.first + map { |element| element[key] } end end diff --git a/guides/source/active_record_postgresql.md b/guides/source/active_record_postgresql.md index 57864e002c..ece990ed50 100644 --- a/guides/source/active_record_postgresql.md +++ b/guides/source/active_record_postgresql.md @@ -318,9 +318,9 @@ You can use `uuid` type to define references in migrations: ```ruby # db/migrate/20150418012400_create_blog.rb enable_extension 'pgcrypto' unless extension_enabled?('pgcrypto') -create_table :posts, id: :uuid, default: 'gen_random_uuid()' +create_table :posts, id: :uuid -create_table :comments, id: :uuid, default: 'gen_random_uuid()' do |t| +create_table :comments, id: :uuid do |t| # t.belongs_to :post, type: :uuid t.references :post, type: :uuid end @@ -414,7 +414,7 @@ extension to generate random UUIDs. ```ruby # db/migrate/20131220144913_create_devices.rb enable_extension 'pgcrypto' unless extension_enabled?('pgcrypto') -create_table :devices, id: :uuid, default: 'gen_random_uuid()' do |t| +create_table :devices, id: :uuid do |t| t.string :kind end diff --git a/guides/source/active_storage_overview.md b/guides/source/active_storage_overview.md index 0b630f9901..c00331b94f 100644 --- a/guides/source/active_storage_overview.md +++ b/guides/source/active_storage_overview.md @@ -151,7 +151,7 @@ and `region` keys in the example above. The S3 Service supports all of the authentication options described in the [AWS SDK documentation] (https://docs.aws.amazon.com/sdk-for-ruby/v3/developer-guide/setup-config.html). -To connect to an S3-compatible object storage API such as Digital Ocean Spaces, provide the `endpoint`: +To connect to an S3-compatible object storage API such as DigitalOcean Spaces, provide the `endpoint`: ```yaml digitalocean: @@ -500,7 +500,7 @@ message.video.open do |file| end ``` -It's important to know that the file are not yet available in the `after_create` callback but in the `after_create_commit` only. +It's important to know that the file is not yet available in the `after_create` callback but in the `after_create_commit` only. Analyzing Files --------------- diff --git a/guides/source/api_app.md b/guides/source/api_app.md index 19ba7c5dbd..10573b64c2 100644 --- a/guides/source/api_app.md +++ b/guides/source/api_app.md @@ -93,7 +93,7 @@ Handled at the Action Pack layer: means not having to spend time thinking about how to model your API in terms of HTTP. - URL Generation: The flip side of routing is URL generation. A good API based - on HTTP includes URLs (see [the GitHub Gist API](https://developer.github.com/v3/gists/) + on HTTP includes URLs (see [the GitHub Gist API](https://docs.github.com/en/rest/reference/gists) for an example). - Header and Redirection Responses: `head :no_content` and `redirect_to user_url(current_user)` come in handy. Sure, you could manually @@ -350,8 +350,12 @@ Instead of the initializer, you'll have to set the relevant options somewhere be built (like `config/application.rb`) and pass them to your preferred middleware, like this: ```ruby -config.session_store :cookie_store, key: '_interslice_session' # <-- this also configures session_options for use below -config.middleware.use ActionDispatch::Cookies # Required for all session management (regardless of session_store) +# This also configures session_options for use below +config.session_store :cookie_store, key: '_interslice_session' + +# Required for all session management (regardless of session_store) +config.middleware.use ActionDispatch::Cookies + config.middleware.use config.session_store, config.session_options ``` @@ -405,7 +409,7 @@ controller modules by default: more information regarding this). - `ActionController::ParamsWrapper`: Wraps the parameters hash into a nested hash, so that you don't have to specify root elements sending POST requests for instance. -- `ActionController::Head`: Support for returning a response with no content, only headers +- `ActionController::Head`: Support for returning a response with no content, only headers. Other plugins may add additional modules. You can get a list of all modules included into `ActionController::API` in the rails console: @@ -433,21 +437,22 @@ Some common modules you might want to add: - `AbstractController::Translation`: Support for the `l` and `t` localization and translation methods. - Support for basic, digest, or token HTTP authentication: - * `ActionController::HttpAuthentication::Basic::ControllerMethods`, - * `ActionController::HttpAuthentication::Digest::ControllerMethods`, + * `ActionController::HttpAuthentication::Basic::ControllerMethods` + * `ActionController::HttpAuthentication::Digest::ControllerMethods` * `ActionController::HttpAuthentication::Token::ControllerMethods` - `ActionView::Layouts`: Support for layouts when rendering. - `ActionController::MimeResponds`: Support for `respond_to`. - `ActionController::Cookies`: Support for `cookies`, which includes support for signed and encrypted cookies. This requires the cookies middleware. -- `ActionController::Caching`: Support view caching for the API controller. Please notice that - you will need to manually specify cache store inside the controller like: - ```ruby - class ApplicationController < ActionController::API - include ::ActionController::Caching - self.cache_store = :mem_cache_store - end - ``` +- `ActionController::Caching`: Support view caching for the API controller. Please note + that you will need to manually specify the cache store inside the controller like this: + + ```ruby + class ApplicationController < ActionController::API + include ::ActionController::Caching + self.cache_store = :mem_cache_store + end + ``` Rails does *not* pass this configuration automatically. The best place to add a module is in your `ApplicationController`, but you can diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md index 08b2743cc3..14f388730c 100644 --- a/guides/source/association_basics.md +++ b/guides/source/association_basics.md @@ -118,7 +118,7 @@ end ``` When used alone, `belongs_to` produces a one-directional one-to-one connection. Therefore each book in the above example "knows" its author, but the authors don't know about their books. -To setup a [bi-directional association](#bi-directional-associations) - use `belongs_to` in combination with a `has_one` or `has_many` on the other model. +To setup a [bi-directional association](#bi-directional-associations) - use `belongs_to` in combination with a `has_one` or `has_many` on the other model. `belongs_to` does not ensure reference consistency, so depending on the use case, you might also need to add a database-level foreign key constraint on the reference column, like this: @@ -356,7 +356,9 @@ end ### The `has_and_belongs_to_many` Association -A `has_and_belongs_to_many` association creates a direct many-to-many connection with another model, with no intervening model. For example, if your application includes assemblies and parts, with each assembly having many parts and each part appearing in many assemblies, you could declare the models this way: +A `has_and_belongs_to_many` association creates a direct many-to-many connection with another model, with no intervening model. +This association indicates that each instance of the declaring model refers to zero or more instances of another model. +For example, if your application includes assemblies and parts, with each assembly having many parts and each part appearing in many assemblies, you could declare the models this way: ```ruby class Assembly < ApplicationRecord diff --git a/guides/source/command_line.md b/guides/source/command_line.md index cf52d6abfc..3b3e2a2cec 100644 --- a/guides/source/command_line.md +++ b/guides/source/command_line.md @@ -393,6 +393,12 @@ With the `helper` method it is possible to access Rails and your application's h INFO: You can also use the alias "db" to invoke the dbconsole: `bin/rails db`. +If you are using multiple databases, `bin/rails dbconsole` will connect to the primary database by default. You can specify which database to connect to using `--database` or `--db`: + +```bash +$ bin/rails dbconsole --database=animals +``` + ### `bin/rails runner` `runner` runs Ruby code in the context of Rails non-interactively. For instance: diff --git a/guides/source/configuring.md b/guides/source/configuring.md index 92ae25bad8..4199b71e8d 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -161,7 +161,7 @@ defaults to `:debug` for all environments. The available log levels are: `:debug * `config.time_zone` sets the default time zone for the application and enables time zone awareness for Active Record. -* `config.autoloader` sets the autoloading mode. This option defaults to `:zeitwerk` if `6.0` is specified in `config.load_defaults`. Applications can still use the classic autoloader by setting this value to `:classic` after loading the framework defaults: +* `config.autoloader` sets the autoloading mode. This option defaults to `:zeitwerk` when `config.load_defaults` is called with `6.0` or greater. Applications can still use the classic autoloader by setting this value to `:classic` after loading the framework defaults: ```ruby config.load_defaults 6.0 @@ -263,7 +263,7 @@ Every Rails application comes with a standard set of middleware which it uses in # `beta1.product.com`. Rails.application.config.hosts << /.*\.product\.com/ ``` - + The provided regexp will be wrapped with both anchors (`\A` and `\z`) so it must match the entire hostname. `/product.com/`, for example, once anchored, would fail to match `www.product.com`. @@ -460,10 +460,9 @@ in controllers and views. This defaults to `false`. to be reused when the object being cached of type `ActiveRecord::Relation` changes by moving the volatile information (max updated at and count) of the relation's cache key into the cache version to support recycling cache key. - Defaults to `false`. * `config.active_record.has_many_inversing` enables setting the inverse record - when traversing `belongs_to` to `has_many` associations. Defaults to `false`. + when traversing `belongs_to` to `has_many` associations. The MySQL adapter adds one additional configuration option: @@ -509,9 +508,9 @@ The schema dumper adds two additional configuration options: * `config.action_controller.per_form_csrf_tokens` configures whether CSRF tokens are only valid for the method/action they were generated for. -* `config.action_controller.default_protect_from_forgery` determines whether forgery protection is added on `ActionController::Base`. This is false by default. +* `config.action_controller.default_protect_from_forgery` determines whether forgery protection is added on `ActionController::Base`. -* `config.action_controller.urlsafe_csrf_tokens` configures whether generated CSRF tokens are URL-safe. Defaults to `false`. +* `config.action_controller.urlsafe_csrf_tokens` configures whether generated CSRF tokens are URL-safe. * `config.action_controller.relative_url_root` can be used to tell Rails that you are [deploying to a subdirectory](configuring.html#deploy-to-a-subdirectory-relative-url-root). The default is `ENV['RAILS_RELATIVE_URL_ROOT']`. @@ -629,8 +628,8 @@ Defaults to `'signed cookie'`. header without modification. Defaults to `false`. * `config.action_dispatch.cookies_same_site_protection` configures the default - value of the `SameSite` attribute when setting cookies. Defaults to `nil`, - which means the `SameSite` attribute is not added. + value of the `SameSite` attribute when setting cookies. When set to `nil`, the + `SameSite` attribute is not added. * `config.action_dispatch.ssl_default_redirect_status` configures the default HTTP status code used when redirecting non-GET/HEAD requests from HTTP to HTTPS @@ -688,7 +687,7 @@ Defaults to `'signed cookie'`. * `config.action_view.form_with_generates_remote_forms` determines whether `form_with` generates remote forms or not. This defaults to `true`. -* `config.action_view.form_with_generates_ids` determines whether `form_with` generates ids on inputs. This defaults to `false`. +* `config.action_view.form_with_generates_ids` determines whether `form_with` generates ids on inputs. * `config.action_view.default_enforce_utf8` determines whether forms are generated with a hidden tag that forces older versions of Internet Explorer to submit forms encoded in UTF-8. This defaults to `false`. @@ -795,7 +794,7 @@ There are a number of settings available on `config.action_mailer`: * `config.action_mailer.perform_caching` specifies whether the mailer templates should perform fragment caching or not. If it's not specified, the default will be `true`. -* `config.action_mailer.delivery_job` specifies delivery job for mail. Defaults to `ActionMailer::DeliveryJob`. +* `config.action_mailer.delivery_job` specifies delivery job for mail. ### Configuring Active Support @@ -812,9 +811,9 @@ There are a few configuration options available in Active Support: * `config.active_support.time_precision` sets the precision of JSON encoded time values. Defaults to `3`. -* `config.active_support.use_sha1_digests` specifies whether to use SHA-1 instead of MD5 to generate non-sensitive digests, such as the ETag header. Defaults to false. +* `config.active_support.use_sha1_digests` specifies whether to use SHA-1 instead of MD5 to generate non-sensitive digests, such as the ETag header. -* `config.active_support.use_authenticated_message_encryption` specifies whether to use AES-256-GCM authenticated encryption as the default cipher for encrypting messages instead of AES-256-CBC. This is false by default. +* `config.active_support.use_authenticated_message_encryption` specifies whether to use AES-256-GCM authenticated encryption as the default cipher for encrypting messages instead of AES-256-CBC. * `ActiveSupport::Logger.silencer` is set to `false` to disable the ability to silence logging in a block. The default is `true`. @@ -832,7 +831,7 @@ There are a few configuration options available in Active Support: * `ActiveSupport.utc_to_local_returns_utc_offset_times` configures `ActiveSupport::TimeZone.utc_to_local` to return a time with a UTC offset - instead of a UTC time incorporating that offset. Defaults to `false`. + instead of a UTC time incorporating that offset. ### Configuring Active Job @@ -889,15 +888,15 @@ There are a few configuration options available in Active Support: * `config.active_job.custom_serializers` allows to set custom argument serializers. Defaults to `[]`. -* `config.active_job.return_false_on_aborted_enqueue` change the return value of `#enqueue` to false instead of the job instance when the enqueuing is aborted. Defaults to `false`. +* `config.active_job.return_false_on_aborted_enqueue` change the return value of `#enqueue` to false instead of the job instance when the enqueuing is aborted. * `config.active_job.log_arguments` controls if the arguments of a job are logged. Defaults to `true`. -* `config.active_job.retry_jitter` controls the amount of "jitter" (random variation) applied to the delay time calculated when retrying failed jobs. Defaults to `0.0`. +* `config.active_job.retry_jitter` controls the amount of "jitter" (random variation) applied to the delay time calculated when retrying failed jobs. * `config.active_job.skip_after_callbacks_if_terminated` controls whether `after_enqueue` / `after_perform` callbacks run when a `before_enqueue` / - `before_perform` callback halts with `throw :abort`. Defaults to `false`. + `before_perform` callback halts with `throw :abort`. ### Configuring Action Cable @@ -940,7 +939,7 @@ You can find more detailed configuration options in the * `config.active_storage.content_types_to_serve_as_binary` accepts an array of strings indicating the content types that Active Storage will always serve as an attachment, rather than inline. The default is `%w(text/html text/javascript image/svg+xml application/postscript application/x-shockwave-flash text/xml application/xml application/xhtml+xml application/mathml+xml text/cache-manifest)`. -* `config.active_storage.content_types_allowed_inline` accepts an array of strings indicating the content types that Active Storage allows to serve as inline. The default is `%w(image/png image/gif image/jpg image/jpeg image/vnd.adobe.photoshop image/vnd.microsoft.icon application/pdf)`. +* `config.active_storage.content_types_allowed_inline` accepts an array of strings indicating the content types that Active Storage allows to serve as inline. The default is `%w(image/png image/gif image/jpg image/jpeg image/tiff image/bmp image/vnd.adobe.photoshop image/vnd.microsoft.icon application/pdf)`. * `config.active_storage.queues.analysis` accepts a symbol indicating the Active Job queue to use for analysis jobs. When this option is `nil`, analysis jobs are sent to the default Active Job queue (see `config.active_job.default_queue_name`). @@ -999,7 +998,7 @@ text/javascript image/svg+xml application/postscript application/x-shockwave-fla `config.load_defaults` sets new defaults up to and including the version passed. Such that passing, say, '6.0' also gets the new defaults from every version before it. -#### For '6.1', new defaults from previous versions below and: +#### For '6.1', defaults from previous versions below and: - `config.active_record.has_many_inversing`: `true` - `config.active_storage.track_variants`: `true` @@ -1010,7 +1009,7 @@ text/javascript image/svg+xml application/postscript application/x-shockwave-fla - `ActiveSupport.utc_to_local_returns_utc_offset_times`: `true` - `config.action_controller.urlsafe_csrf_tokens`: `true` -#### For '6.0', new defaults from previous versions below and: +#### For '6.0', defaults from previous versions below and: - `config.autoloader`: `:zeitwerk` - `config.action_view.default_enforce_utf8`: `false` @@ -1023,7 +1022,7 @@ text/javascript image/svg+xml application/postscript application/x-shockwave-fla - `config.active_storage.replace_on_assign_to_many`: `true` - `config.active_record.collection_cache_versioning`: `true` -#### For '5.2', new defaults from previous versions below and: +#### For '5.2', defaults from previous versions below and: - `config.active_record.cache_versioning`: `true` - `config.action_dispatch.use_authenticated_cookie_encryption`: `true` @@ -1032,12 +1031,12 @@ text/javascript image/svg+xml application/postscript application/x-shockwave-fla - `config.action_controller.default_protect_from_forgery`: `true` - `config.action_view.form_with_generates_ids`: `true` -#### For '5.1', new defaults from previous versions below and: +#### For '5.1', defaults from previous versions below and: - `config.assets.unknown_asset_fallback`: `false` - `config.action_view.form_with_generates_remote_forms`: `true` -#### For '5.0': +#### For '5.0', baseline defaults from below and: - `config.action_controller.per_form_csrf_tokens`: `true` - `config.action_controller.forgery_protection_origin_check`: `true` @@ -1045,6 +1044,22 @@ text/javascript image/svg+xml application/postscript application/x-shockwave-fla - `config.active_record.belongs_to_required_by_default`: `true` - `config.ssl_options`: `{ hsts: { subdomains: true } }` +#### Baseline defaults: + +- `config.action_controller.default_protect_from_forgery`: `false` +- `config.action_controller.urlsafe_csrf_tokens`: `false` +- `config.action_dispatch.cookies_same_site_protection`: `nil` +- `config.action_mailer.delivery_job`: `ActionMailer::DeliveryJob` +- `config.action_view.form_with_generates_ids`: `false` +- `config.active_job.retry_jitter`: `0.0` +- `config.active_job.return_false_on_aborted_enqueue`: `false` +- `config.active_job.skip_after_callbacks_if_terminated`: `false` +- `config.active_record.collection_cache_versioning`: `false` +- `config.active_record.has_many_inversing`: `false` +- `config.active_support.use_authenticated_message_encryption`: `false` +- `config.active_support.use_sha1_digests`: `false` +- `ActiveSupport.utc_to_local_returns_utc_offset_times`: `false` + ### Configuring a Database Just about every Rails application will interact with a database. You can connect to the database by setting an environment variable `ENV['DATABASE_URL']` or by using a configuration file called `config/database.yml`. diff --git a/guides/source/routing.md b/guides/source/routing.md index 93847f1736..00ec2ed845 100644 --- a/guides/source/routing.md +++ b/guides/source/routing.md @@ -751,6 +751,35 @@ end Both the `matches?` method and the lambda gets the `request` object as an argument. +#### Constraints in a block form + +You can specify constraints in a block form. This is useful for when you need to apply the same rule to several routes. For example + +``` +class RestrictedListConstraint + # ...Same as the example above +end + +Rails.application.routes.draw do + constraints(RestrictedListConstraint.new) do + get '*path', to: 'restricted_list#index', + get '*other-path', to: 'other_restricted_list#index', + end +end +``` + +You also use a `lambda`: + +``` +Rails.application.routes.draw do + constraints(lambda { |request| RestrictedList.retrieve_ips.include?(request.remote_ip) }) do + get '*path', to: 'restricted_list#index', + get '*other-path', to: 'other_restricted_list#index', + end +end +``` + + ### Route Globbing and Wildcard Segments Route globbing is a way to specify that a particular parameter should be matched to all the remaining parts of a route. For example: diff --git a/guides/source/security.md b/guides/source/security.md index b15c7a14db..3a3bad270e 100644 --- a/guides/source/security.md +++ b/guides/source/security.md @@ -292,13 +292,13 @@ There are many other possibilities, like using a `