Merge branch 'master' into combine-structure-and-schema-tasks
This commit is contained in:
commit
9a36b50b84
|
@ -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:
|
||||
|
||||
|
|
|
@ -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*
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 %(<script src="/javascripts/foo.js"></script>), 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 = "<http://example.com/style.css>; rel=preload; as=style,<http://example.com/all.js>; 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
|
||||
|
|
|
@ -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 #<ActionView::Base:0x[0-9a-f]+>/, 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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -36,7 +36,6 @@ require "active_record/errors"
|
|||
module ActiveRecord
|
||||
extend ActiveSupport::Autoload
|
||||
|
||||
autoload :AdvisoryLockBase
|
||||
autoload :Base
|
||||
autoload :Callbacks
|
||||
autoload :Core
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -136,7 +136,6 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def deserialize(value)
|
||||
return if value.nil?
|
||||
mapping.key(subtype.deserialize(value))
|
||||
end
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -12,5 +12,5 @@ mary_stay_home:
|
|||
|
||||
steve_connecting_the_dots:
|
||||
name: Connecting The Dots
|
||||
writer_type: Man
|
||||
writer_type: Human
|
||||
writer_id: Steve
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class MixedCaseMonkey < ActiveRecord::Base
|
||||
belongs_to :man
|
||||
belongs_to :human
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
---------------
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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`.
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -292,13 +292,13 @@ There are many other possibilities, like using a `<script>` tag to make a cross-
|
|||
|
||||
NOTE: We can't distinguish a `<script>` tag's origin—whether it's a tag on your own site or on some other malicious site—so we must block all `<script>` across the board, even if it's actually a safe same-origin script served from your own site. In these cases, explicitly skip CSRF protection on actions that serve JavaScript meant for a `<script>` tag.
|
||||
|
||||
To protect against all other forged requests, we introduce a _required security token_ that our site knows but other sites don't know. We include the security token in requests and verify it on the server. This is a one-liner in your application controller, and is the default for newly created Rails applications:
|
||||
To protect against all other forged requests, we introduce a _required security token_ that our site knows but other sites don't know. We include the security token in requests and verify it on the server. This is done automatically when `config.action_controller.default_protect_from_forgery` is set to `true`, which is the default for newly created Rails applications. You can also do it manually by adding the following to your application controller:
|
||||
|
||||
```ruby
|
||||
protect_from_forgery with: :exception
|
||||
```
|
||||
|
||||
This will automatically include a security token in all forms and Ajax requests generated by Rails. If the security token doesn't match what was expected, an exception will be thrown.
|
||||
This will include a security token in all forms and Ajax requests generated by Rails. If the security token doesn't match what was expected, an exception will be thrown.
|
||||
|
||||
NOTE: By default, Rails includes an [unobtrusive scripting adapter](https://github.com/rails/rails/blob/master/actionview/app/assets/javascripts),
|
||||
which adds a header called `X-CSRF-Token` with the security token on every non-GET
|
||||
|
|
|
@ -320,7 +320,7 @@ specify to make your test failure messages clearer.
|
|||
| `assert_in_delta( expected, actual, [delta], [msg] )` | Ensures that the numbers `expected` and `actual` are within `delta` of each other.|
|
||||
| `assert_not_in_delta( expected, actual, [delta], [msg] )` | Ensures that the numbers `expected` and `actual` are not within `delta` of each other.|
|
||||
| `assert_in_epsilon ( expected, actual, [epsilon], [msg] )` | Ensures that the numbers `expected` and `actual` have a relative error less than `epsilon`.|
|
||||
| `assert_not_in_epsilon ( expected, actual, [epsilon], [msg] )` | Ensures that the numbers `expected` and `actual` don't have a relative error less than `epsilon`.|
|
||||
| `assert_not_in_epsilon ( expected, actual, [epsilon], [msg] )` | Ensures that the numbers `expected` and `actual` have a relative error not less than `epsilon`.|
|
||||
| `assert_throws( symbol, [msg] ) { block }` | Ensures that the given block throws the symbol.|
|
||||
| `assert_raises( exception1, exception2, ... ) { block }` | Ensures that the given block raises one of the given exceptions.|
|
||||
| `assert_instance_of( class, obj, [msg] )` | Ensures that `obj` is an instance of `class`.|
|
||||
|
|
|
@ -25,10 +25,14 @@ module Rails
|
|||
directory "app"
|
||||
empty_directory_with_keep_file "app/assets/images/#{namespaced_name}"
|
||||
end
|
||||
|
||||
remove_dir "app/mailers" if options[:skip_action_mailer]
|
||||
remove_dir "app/jobs" if options[:skip_active_job]
|
||||
elsif full?
|
||||
empty_directory_with_keep_file "app/models"
|
||||
empty_directory_with_keep_file "app/controllers"
|
||||
empty_directory_with_keep_file "app/mailers"
|
||||
empty_directory_with_keep_file "app/mailers" unless options[:skip_action_mailer]
|
||||
empty_directory_with_keep_file "app/jobs" unless options[:skip_active_job]
|
||||
|
||||
unless api?
|
||||
empty_directory_with_keep_file "app/assets/images/#{namespaced_name}"
|
||||
|
|
|
@ -232,6 +232,24 @@ class PluginGeneratorTest < Rails::Generators::TestCase
|
|||
end
|
||||
end
|
||||
|
||||
def test_skip_action_mailer_and_skip_active_job_with_mountable
|
||||
run_generator [destination_root, "--mountable", "--skip-action-mailer", "--skip-active-job"]
|
||||
assert_no_directory "app/mailers"
|
||||
assert_no_directory "app/jobs"
|
||||
end
|
||||
|
||||
def test_skip_action_mailer_and_skip_active_job_with_api_and_mountable
|
||||
run_generator [destination_root, "--api", "--mountable", "--skip-action-mailer", "--skip-active-job"]
|
||||
assert_no_directory "app/mailers"
|
||||
assert_no_directory "app/jobs"
|
||||
end
|
||||
|
||||
def test_skip_action_mailer_and_skip_active_job_with_full
|
||||
run_generator [destination_root, "--full", "--skip-action-mailer", "--skip-active-job"]
|
||||
assert_no_directory "app/mailers"
|
||||
assert_no_directory "app/jobs"
|
||||
end
|
||||
|
||||
def test_template_from_dir_pwd
|
||||
FileUtils.cd(Rails.root)
|
||||
assert_match(/It works from file!/, run_generator([destination_root, "-m", "lib/template.rb"]))
|
||||
|
@ -268,6 +286,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase
|
|||
assert_file "app/views"
|
||||
assert_file "app/helpers"
|
||||
assert_file "app/mailers"
|
||||
assert_file "app/jobs"
|
||||
assert_file "bin/rails", /\s+require\s+["']rails\/all["']/
|
||||
assert_file "config/routes.rb", /Rails.application.routes.draw do/
|
||||
assert_file "lib/bukkits/engine.rb", /module Bukkits\n class Engine < ::Rails::Engine\n end\nend/
|
||||
|
@ -284,6 +303,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase
|
|||
assert_file "hyphenated-name/app/views"
|
||||
assert_file "hyphenated-name/app/helpers"
|
||||
assert_file "hyphenated-name/app/mailers"
|
||||
assert_file "hyphenated-name/app/jobs"
|
||||
assert_file "hyphenated-name/bin/rails"
|
||||
assert_file "hyphenated-name/config/routes.rb", /Rails.application.routes.draw do/
|
||||
assert_file "hyphenated-name/lib/hyphenated/name/engine.rb", /module Hyphenated\n module Name\n class Engine < ::Rails::Engine\n end\n end\nend/
|
||||
|
@ -301,6 +321,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase
|
|||
assert_file "my_hyphenated-name/app/views"
|
||||
assert_file "my_hyphenated-name/app/helpers"
|
||||
assert_file "my_hyphenated-name/app/mailers"
|
||||
assert_file "my_hyphenated-name/app/jobs"
|
||||
assert_file "my_hyphenated-name/bin/rails"
|
||||
assert_file "my_hyphenated-name/config/routes.rb", /Rails\.application\.routes\.draw do/
|
||||
assert_file "my_hyphenated-name/lib/my_hyphenated/name/engine.rb", /module MyHyphenated\n module Name\n class Engine < ::Rails::Engine\n end\n end\nend/
|
||||
|
|
Loading…
Reference in New Issue