Merge branch 'main' into add-rails-development-hosts-env-variable

This commit is contained in:
Debbie Milburn 2021-03-09 11:59:11 -05:00 committed by GitHub
commit 03aa08526a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
102 changed files with 826 additions and 764 deletions

View File

@ -19,7 +19,7 @@ class ActionMailbox::Ingresses::Mandrill::InboundEmailsControllerTest < ActionDi
test "receiving an inbound email from Mandrill" do
assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
post rails_mandrill_inbound_emails_url,
headers: { "X-Mandrill-Signature" => "gldscd2tAb/G+DmpiLcwukkLrC4=" }, params: { mandrill_events: @events }
headers: { "X-Mandrill-Signature" => "1bNbyqkMFL4VYIT5+RQCrPs/mRY=" }, params: { mandrill_events: @events }
end
assert_response :ok

View File

@ -27,7 +27,7 @@ Content-Type: multipart/related;
Content-Transfer-Encoding: base64
Content-Disposition: inline;
filename=avatar1.jpeg
Content-Type: image/jpg;
Content-Type: image/jpeg;
name="avatar1.jpeg"
Content-Id: <7AAEB353-2341-4D46-A054-5CA5CB2363B7>
@ -397,7 +397,7 @@ mk8VWW5WRGJGAOaAP//Z
Content-Transfer-Encoding: base64
Content-Disposition: inline;
filename=avatar2.jpg
Content-Type: image/jpg;
Content-Type: image/jpeg;
x-unix-mode=0700;
name="avatar2.jpg"
Content-Id: <4594E827-6E69-4329-8691-6BC35E3E73A0>

View File

@ -433,7 +433,7 @@ module ActionMailer
# * <tt>deliveries</tt> - Keeps an array of all the emails sent out through the Action Mailer with
# <tt>delivery_method :test</tt>. Most useful for unit and functional testing.
#
# * <tt>deliver_later_queue_name</tt> - The name of the queue used with <tt>deliver_later</tt>. Defaults to +mailers+.
# * <tt>deliver_later_queue_name</tt> - The name of the queue used with <tt>deliver_later</tt>.
class Base < AbstractController::Base
include DeliveryMethods
include Rescuable

View File

@ -103,7 +103,7 @@ module ActionController #:nodoc:
# If you need to use a MIME type which isn't supported by default, you can register your own handlers in
# +config/initializers/mime_types.rb+ as follows.
#
# Mime::Type.register "image/jpg", :jpg
# Mime::Type.register "image/jpeg", :jpg
#
# +respond_to+ also allows you to specify a common block for different formats by using +any+:
#

View File

@ -303,15 +303,15 @@ module ActionController #:nodoc:
[form_authenticity_param, request.x_csrf_token]
end
# Sets the token value for the current session.
def form_authenticity_token(form_options: {})
# Creates the authenticity token for the current request.
def form_authenticity_token(form_options: {}) # :doc:
masked_authenticity_token(session, form_options: form_options)
end
# Creates a masked version of the authenticity token that varies
# on each request. The masking is used to mitigate SSL attacks
# like BREACH.
def masked_authenticity_token(session, form_options: {}) # :doc:
def masked_authenticity_token(session, form_options: {})
action, method = form_options.values_at(:action, :method)
raw_token = if per_form_csrf_tokens && action && method

View File

@ -1,10 +1,3 @@
<% unless @exception.blamed_files.blank? %>
<% if (hide = @exception.blamed_files.length > 8) %>
<a href="#" onclick="return toggleTrace()">Toggle blamed files</a>
<% end %>
<pre id="blame_trace" <%='style="display:none"' if hide %>><code><%= @exception.describe_blame %></code></pre>
<% end %>
<h2 style="margin-top: 30px">Request</h2>
<% if params_valid? %>
<p><b>Parameters</b>:</p> <pre><%= debug_params(@request.filtered_parameters) %></pre>

View File

@ -153,6 +153,7 @@
font-weight: bold;
margin: 0;
padding: 10px 18px;
cursor: pointer;
-webkit-appearance: none;
}
input[type="submit"]:focus,
@ -238,9 +239,6 @@
var hide = function(id) {
document.getElementById(id).style.display = 'none';
}
var toggleTrace = function() {
return toggle('blame_trace');
}
var toggleSessionDump = function() {
return toggle('session_dump');
}

View File

@ -12,7 +12,7 @@ class ActionController::Base
def before_actions
filters = _process_action_callbacks.select { |c| c.kind == :before }
filters.map!(&:raw_filter)
filters.map!(&:filter)
end
end
end

View File

@ -1229,7 +1229,7 @@ class IntegrationFileUploadTest < ActionDispatch::IntegrationTest
def test_fixture_file_upload
post "/test_file_upload",
params: {
file: fixture_file_upload("/ruby_on_rails.jpg", "image/jpg")
file: fixture_file_upload("/ruby_on_rails.jpg", "image/jpeg")
}
assert_equal "45142", @response.body
end

View File

@ -909,7 +909,7 @@ XML
def test_fixture_file_upload_with_binary
filename = "ruby_on_rails.jpg"
path = "#{FILES_DIR}/#{filename}"
content_type = "image/jpg"
content_type = "image/jpeg"
binary_file_upload = fixture_file_upload(path, content_type, :binary)
assert_equal File.open(path, READ_BINARY).read, binary_file_upload.read
@ -919,14 +919,14 @@ XML
end
def test_fixture_file_upload_should_be_able_access_to_tempfile
file = fixture_file_upload(FILES_DIR + "/ruby_on_rails.jpg", "image/jpg")
file = fixture_file_upload(FILES_DIR + "/ruby_on_rails.jpg", "image/jpeg")
assert_respond_to file, :tempfile
end
def test_fixture_file_upload
post :test_file_upload,
params: {
file: fixture_file_upload(FILES_DIR + "/ruby_on_rails.jpg", "image/jpg")
file: fixture_file_upload(FILES_DIR + "/ruby_on_rails.jpg", "image/jpeg")
}
assert_equal "45142", @response.body
end
@ -935,7 +935,7 @@ XML
TestCaseTest.stub :fixture_path, File.expand_path("../fixtures", __dir__) do
TestCaseTest.stub :file_fixture_path, nil do
assert_deprecated(/In Rails 7.0, the path needs to be relative to `file_fixture_path`/) do
fixture_file_upload("multipart/ruby_on_rails.jpg", "image/jpg")
fixture_file_upload("multipart/ruby_on_rails.jpg", "image/jpeg")
end
end
end
@ -944,7 +944,7 @@ XML
def test_fixture_file_upload_does_not_output_deprecation_when_file_fixture_path_is_set
TestCaseTest.stub :fixture_path, File.expand_path("../fixtures", __dir__) do
assert_not_deprecated do
fixture_file_upload("ruby_on_rails.jpg", "image/jpg")
fixture_file_upload("ruby_on_rails.jpg", "image/jpeg")
end
end
end
@ -954,7 +954,7 @@ XML
expected = "`fixture_file_upload(\"multipart/ruby_on_rails.jpg\")` to `fixture_file_upload(\"ruby_on_rails.jpg\")`"
assert_deprecated(expected) do
uploaded_file = fixture_file_upload("multipart/ruby_on_rails.jpg", "image/jpg")
uploaded_file = fixture_file_upload("multipart/ruby_on_rails.jpg", "image/jpeg")
assert_equal File.open("#{FILES_DIR}/ruby_on_rails.jpg", READ_PLAIN).read, uploaded_file.read
end
end
@ -975,13 +975,13 @@ XML
def test_fixture_file_upload_ignores_fixture_path_given_full_path
TestCaseTest.stub :fixture_path, __dir__ do
uploaded_file = fixture_file_upload("#{FILES_DIR}/ruby_on_rails.jpg", "image/jpg")
uploaded_file = fixture_file_upload("#{FILES_DIR}/ruby_on_rails.jpg", "image/jpeg")
assert_equal File.open("#{FILES_DIR}/ruby_on_rails.jpg", READ_PLAIN).read, uploaded_file.read
end
end
def test_fixture_file_upload_ignores_nil_fixture_path
uploaded_file = fixture_file_upload("#{FILES_DIR}/ruby_on_rails.jpg", "image/jpg")
uploaded_file = fixture_file_upload("#{FILES_DIR}/ruby_on_rails.jpg", "image/jpeg")
assert_equal File.open("#{FILES_DIR}/ruby_on_rails.jpg", READ_PLAIN).read, uploaded_file.read
end
@ -989,7 +989,7 @@ XML
filename = "ruby_on_rails.jpg"
path = "#{FILES_DIR}/#{filename}"
post :test_file_upload, params: {
file: Rack::Test::UploadedFile.new(path, "image/jpg", true)
file: Rack::Test::UploadedFile.new(path, "image/jpeg", true)
}
assert_equal "45142", @response.body
end

View File

@ -157,7 +157,7 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest
test "uploads and reads binary file" do
with_test_routing do
fixture = FIXTURE_PATH + "/ruby_on_rails.jpg"
params = { uploaded_data: fixture_file_upload(fixture, "image/jpg") }
params = { uploaded_data: fixture_file_upload(fixture, "image/jpeg") }
post "/read", params: params
end
end

View File

@ -1,3 +1,7 @@
* Add `config.action_text.attachment_tag_name`, to specify the HTML tag that contains attachments.
*Mark VanLandingham*
* Expose how we render the HTML _surrounding_ rich text content as an
extensible `layouts/action_view/contents/_content.html.erb` template to
encourage user-land customizations, while retaining private API control over how

View File

@ -5,7 +5,7 @@ require "rails-html-sanitizer"
module ActionText
module ContentHelper
mattr_accessor(:sanitizer) { Rails::Html::Sanitizer.safe_list_sanitizer.new }
mattr_accessor(:allowed_tags) { sanitizer.class.allowed_tags + [ ActionText::Attachment::TAG_NAME, "figure", "figcaption" ] }
mattr_accessor(:allowed_tags) { sanitizer.class.allowed_tags + [ ActionText::Attachment.tag_name, "figure", "figcaption" ] }
mattr_accessor(:allowed_attributes) { sanitizer.class.allowed_attributes + ActionText::Attachment::ATTRIBUTES }
mattr_accessor(:scrubber)

View File

@ -6,8 +6,8 @@ module ActionText
class Attachment
include Attachments::TrixConversion, Attachments::Minification, Attachments::Caching
TAG_NAME = "action-text-attachment"
SELECTOR = TAG_NAME
mattr_accessor :tag_name, default: "action-text-attachment"
ATTRIBUTES = %w( sgid content-type url href filename filesize width height previewable presentation caption )
class << self
@ -38,7 +38,7 @@ module ActionText
private
def node_from_attributes(attributes)
if attributes = process_attributes(attributes).presence
ActionText::HtmlConversion.create_element(TAG_NAME, attributes)
ActionText::HtmlConversion.create_element(tag_name, attributes)
end
end

View File

@ -57,7 +57,7 @@ module ActionText
end
TAG_NAME = "div"
ATTACHMENT_SELECTOR = "#{ActionText::Attachment::SELECTOR}[presentation=gallery]"
ATTACHMENT_SELECTOR = "#{ActionText::Attachment.tag_name}[presentation=gallery]"
SELECTOR = "#{TAG_NAME}:has(#{ATTACHMENT_SELECTOR} + #{ATTACHMENT_SELECTOR})"
private_constant :TAG_NAME, :ATTACHMENT_SELECTOR, :SELECTOR

View File

@ -7,7 +7,7 @@ module ActionText
class_methods do
def fragment_by_minifying_attachments(content)
Fragment.wrap(content).replace(ActionText::Attachment::SELECTOR) do |node|
Fragment.wrap(content).replace(ActionText::Attachment.tag_name) do |node|
node.tap { |n| n.inner_html = "" }
end
end

View File

@ -58,7 +58,7 @@ module ActionText
end
def render_attachments(**options, &block)
content = fragment.replace(ActionText::Attachment::SELECTOR) do |node|
content = fragment.replace(ActionText::Attachment.tag_name) do |node|
block.call(attachment_for_node(node, **options))
end
self.class.new(content, canonicalize: false)
@ -111,7 +111,7 @@ module ActionText
private
def attachment_nodes
@attachment_nodes ||= fragment.find_all(ActionText::Attachment::SELECTOR)
@attachment_nodes ||= fragment.find_all(ActionText::Attachment.tag_name)
end
def attachment_gallery_nodes

View File

@ -12,6 +12,13 @@ module ActionText
isolate_namespace ActionText
config.eager_load_namespaces << ActionText
config.action_text = ActiveSupport::OrderedOptions.new
config.action_text.attachment_tag_name = "action-text-attachment"
config.autoload_once_paths = %W(
#{root}/app/helpers
#{root}/app/models
)
initializer "action_text.attribute" do
ActiveSupport.on_load(:active_record) do
include ActionText::Attribute
@ -64,5 +71,9 @@ module ActionText
include ActionText::SystemTestHelper
end
end
initializer "action_text.configure" do |app|
ActionText::Attachment.tag_name = app.config.action_text.attachment_tag_name
end
end
end

View File

@ -21,7 +21,7 @@
],
"license": "MIT",
"dependencies": {
"@rails/activestorage": "^6.0.0-alpha"
"@rails/activestorage": "^6.0.0"
},
"peerDependencies": {
"trix": "^1.2.0"

View File

@ -66,6 +66,6 @@ class ActionText::AttachmentTest < ActiveSupport::TestCase
end
def attachable
@attachment ||= create_file_blob(filename: "racecar.jpg", content_type: "image/jpg")
@attachment ||= create_file_blob(filename: "racecar.jpg", content_type: "image/jpeg")
end
end

View File

@ -28,7 +28,7 @@ class ActionText::ContentTest < ActiveSupport::TestCase
end
test "extracts attachables" do
attachable = create_file_blob(filename: "racecar.jpg", content_type: "image/jpg")
attachable = create_file_blob(filename: "racecar.jpg", content_type: "image/jpeg")
html = %Q(<action-text-attachment sgid="#{attachable.attachable_sgid}" caption="Captioned"></action-text-attachment>)
content = content_from_html(html)
@ -56,7 +56,7 @@ class ActionText::ContentTest < ActiveSupport::TestCase
end
test "identifies destroyed attachables as missing" do
attachable = create_file_blob(filename: "racecar.jpg", content_type: "image/jpg")
attachable = create_file_blob(filename: "racecar.jpg", content_type: "image/jpeg")
html = %Q(<action-text-attachment sgid="#{attachable.attachable_sgid}"></action-text-attachment>)
attachable.destroy!
content = content_from_html(html)
@ -78,6 +78,15 @@ class ActionText::ContentTest < ActiveSupport::TestCase
assert_equal '<action-text-attachment sgid="123" content-type="text/plain" width="100" height="100" caption="Captioned"></action-text-attachment>', content.to_html
end
test "converts Trix-formatted attachments with custom tag name" do
with_attachment_tag_name("arbitrary-tag") do
html = %Q(<figure data-trix-attachment='{"sgid":"123","contentType":"text/plain","width":100,"height":100}' data-trix-attributes='{"caption":"Captioned"}'></figure>)
content = content_from_html(html)
assert_equal 1, content.attachments.size
assert_equal '<arbitrary-tag sgid="123" content-type="text/plain" width="100" height="100" caption="Captioned"></arbitrary-tag>', content.to_html
end
end
test "ignores Trix-formatted attachments with malformed JSON" do
html = %Q(<div data-trix-attachment='{"sgid":"garbage...'></div>)
content = content_from_html(html)
@ -131,4 +140,13 @@ class ActionText::ContentTest < ActiveSupport::TestCase
assert_nothing_raised { content.to_s }
end
end
def with_attachment_tag_name(tag_name)
previous_tag_name = ActionText::Attachment.tag_name
ActionText::Attachment.tag_name = tag_name
yield
ensure
ActionText::Attachment.tag_name = previous_tag_name
end
end

View File

@ -32,14 +32,14 @@ class ActionText::ModelTest < ActiveSupport::TestCase
end
test "embed extraction" do
blob = create_file_blob(filename: "racecar.jpg", content_type: "image/jpg")
blob = create_file_blob(filename: "racecar.jpg", content_type: "image/jpeg")
message = Message.create!(subject: "Greetings", content: ActionText::Content.new("Hello world").append_attachables(blob))
assert_equal "racecar.jpg", message.content.embeds.first.filename.to_s
end
test "embed extraction only extracts file attachments" do
remote_image_html = '<action-text-attachment content-type="image" url="http://example.com/cat.jpg"></action-text-attachment>'
blob = create_file_blob(filename: "racecar.jpg", content_type: "image/jpg")
blob = create_file_blob(filename: "racecar.jpg", content_type: "image/jpeg")
content = ActionText::Content.new(remote_image_html).append_attachables(blob)
message = Message.create!(subject: "Greetings", content: content)
assert_equal [ActionText::Attachables::RemoteImage, ActiveStorage::Blob], message.content.body.attachables.map(&:class)
@ -47,7 +47,7 @@ class ActionText::ModelTest < ActiveSupport::TestCase
end
test "embed extraction deduplicates file attachments" do
blob = create_file_blob(filename: "racecar.jpg", content_type: "image/jpg")
blob = create_file_blob(filename: "racecar.jpg", content_type: "image/jpeg")
content = ActionText::Content.new("Hello world").append_attachables([ blob, blob ])
assert_nothing_raised do

View File

@ -646,10 +646,10 @@ module ActionView
# # => <a href="tel:1234567890">1234567890</a>
#
# phone_to "1234567890", "Phone me"
# # => <a href="tel:134567890">Phone me</a>
# # => <a href="tel:1234567890">Phone me</a>
#
# phone_to "1234567890", country_code: "01"
# # => <a href="tel:+015155555785">1234567890</a>
# # => <a href="tel:+011234567890">1234567890</a>
#
# You can use a block as well if your link target is hard to fit into the name parameter. \ERB example:
#

View File

@ -1,3 +1,33 @@
* Type cast enum values by the original attribute type.
The notable thing about this change is that unknown labels will no longer match 0 on MySQL.
```ruby
class Book < ActiveRecord::Base
enum :status, { proposed: 0, written: 1, published: 2 }
end
```
Before:
```ruby
# SELECT `books`.* FROM `books` WHERE `books`.`status` = 'prohibited' LIMIT 1
Book.find_by(status: :prohibited)
# => #<Book id: 1, status: "proposed", ...> (for mysql2 adapter)
# => ActiveRecord::StatementInvalid: PG::InvalidTextRepresentation: ERROR: invalid input syntax for type integer: "prohibited" (for postgresql adapter)
# => nil (for sqlite3 adapter)
```
After:
```ruby
# SELECT `books`.* FROM `books` WHERE `books`.`status` IS NULL LIMIT 1
Book.find_by(status: :prohibited)
# => nil (for all adapters)
```
*Ryuta Kamizono*
* Fixtures for `has_many :through` associations now load timestamps on join tables
Given this fixture:

View File

@ -849,6 +849,11 @@ module ActiveRecord
# person.pets.count # => 1
# person.pets.any? # => true
#
# Calling it without a block when the collection is not yet
# loaded is equivalent to <tt>collection.exists?</tt>.
# If you're going to load the collection anyway, it is better
# to call <tt>collection.load.any?</tt> to avoid an extra query.
#
# You can also pass a +block+ to define criteria. The behavior
# is the same, it returns true if the collection based on the
# criteria is not empty.

View File

@ -97,7 +97,7 @@ module ActiveRecord
include ConnectionAdapters::AbstractPool
attr_accessor :automatic_reconnect, :checkout_timeout
attr_reader :db_config, :size, :reaper, :pool_config, :connection_klass
attr_reader :db_config, :size, :reaper, :pool_config, :connection_klass, :async_executor
delegate :schema_cache, :schema_cache=, to: :pool_config
@ -461,12 +461,14 @@ module ActiveRecord
def build_async_executor
case Base.async_query_executor
when :multi_thread_pool
Concurrent::ThreadPoolExecutor.new(
min_threads: @db_config.min_threads,
max_threads: @db_config.max_threads,
max_queue: @db_config.max_queue,
fallback_policy: :caller_runs
)
if @db_config.max_threads > 0
Concurrent::ThreadPoolExecutor.new(
min_threads: @db_config.min_threads,
max_threads: @db_config.max_threads,
max_queue: @db_config.max_queue,
fallback_policy: :caller_runs
)
end
when :global_thread_pool
Base.global_thread_pool_async_query_executor
end

View File

@ -477,8 +477,7 @@ module ActiveRecord
end
table = Arel::Table.new(table_name)
manager = Arel::InsertManager.new
manager.into(table)
manager = Arel::InsertManager.new(table)
if values_list.size == 1
values = values_list.shift

View File

@ -94,8 +94,8 @@ module ActiveRecord
sql = ["CREATE"]
sql << "UNIQUE" if index.unique
sql << "INDEX"
sql << "IF NOT EXISTS" if o.if_not_exists
sql << o.algorithm if o.algorithm
sql << "IF NOT EXISTS" if o.if_not_exists
sql << index.type if index.type
sql << "#{quote_column_name(index.name)} ON #{quote_table_name(index.table)}"
sql << "USING #{index.using}" if supports_index_using? && index.using

View File

@ -429,8 +429,9 @@ module ActiveRecord
true
end
def async_enabled?
supports_concurrent_connections? && !Base.async_query_executor.nil?
def async_enabled? # :nodoc:
supports_concurrent_connections? &&
!Base.async_query_executor.nil? && !pool.async_executor.nil?
end
# This is meant to be implemented by the adapters that support extensions

View File

@ -161,7 +161,7 @@ module ActiveRecord
# set to +nil+ which will not run queries in the background. Applications must configure
# a thread pool executor to use this feature. Options are:
#
# * nil - Does not initalize a thread pool executor. Any async calls will be
# * nil - Does not initialize a thread pool executor. Any async calls will be
# run in the foreground.
# * :global_thread_pool - Initializes a single +Concurrent::ThreadPoolExecutor+
# that uses the +async_query_concurrency+ for the +max_threads+ value.

View File

@ -115,7 +115,7 @@ module ActiveRecord
end
class EnumType < Type::Value # :nodoc:
delegate :type, to: :subtype
delegate :type, :serializable?, to: :subtype
def initialize(name, mapping, subtype)
@name = name
@ -128,10 +128,8 @@ module ActiveRecord
value.to_s
elsif mapping.has_value?(value)
mapping.key(value)
elsif value.blank?
nil
else
assert_valid_value(value)
value.presence
end
end

View File

@ -120,6 +120,14 @@ module ActiveRecord
# { id: 1, title: "Rework", author: "David" },
# { id: 1, title: "Eloquent Ruby", author: "Russ" }
# ])
#
# # insert_all works on chained scopes, and you can use create_with
# # to set default attributes for all inserted records.
#
# author.books.create_with(created_at: Time.now).insert_all([
# { id: 1, title: "Rework" },
# { id: 2, title: "Eloquent Ruby" }
# ])
def insert_all(attributes, returning: nil, unique_by: nil)
InsertAll.new(self, attributes, on_duplicate: :skip, returning: returning, unique_by: unique_by).execute
end
@ -363,13 +371,12 @@ module ActiveRecord
end
end
im = Arel::InsertManager.new
im.into(arel_table)
im = Arel::InsertManager.new(arel_table)
if values.empty?
im.insert(connection.empty_insert_statement_value(primary_key))
else
im.insert(_substitute_values(values))
im.insert(values.transform_keys { |name| arel_table[name] })
end
connection.insert(im, "#{self} Create", primary_key || false, primary_key_value)
@ -386,9 +393,8 @@ module ActiveRecord
constraints << current_scope.where_clause.ast
end
um = Arel::UpdateManager.new
um.table(arel_table)
um.set(_substitute_values(values))
um = Arel::UpdateManager.new(arel_table)
um.set(values.transform_keys { |name| arel_table[name] })
um.wheres = constraints
connection.update(um, "#{self} Update")
@ -405,8 +411,7 @@ module ActiveRecord
constraints << current_scope.where_clause.ast
end
dm = Arel::DeleteManager.new
dm.from(arel_table)
dm = Arel::DeleteManager.new(arel_table)
dm.wheres = constraints
connection.delete(dm, "#{self} Destroy")
@ -428,12 +433,6 @@ module ActiveRecord
def discriminate_class_for_record(record)
self
end
def _substitute_values(values)
values.map do |name, value|
[ arel_table[name], Arel::Nodes::BindParam.new(value) ]
end
end
end
# Returns true if this object hasn't been saved yet -- that is, a record

View File

@ -310,6 +310,7 @@ module ActiveRecord
if operation != "count"
type = column.try(:type_caster) ||
lookup_cast_type_from_join_dependencies(column_name.to_s) || Type.default_value
type = type.subtype if Enum::EnumType === type
end
type_cast_calculated_value(result.cast_values.first, operation, type)
@ -384,6 +385,7 @@ module ActiveRecord
if operation != "count"
type = column.try(:type_caster) ||
lookup_cast_type_from_join_dependencies(column_name.to_s) || Type.default_value
type = type.subtype if Enum::EnumType === type
end
hash_rows.each_with_object({}) do |row, result|

View File

@ -51,30 +51,25 @@ module ActiveRecord
@rewhere = rewhere
end
NORMAL_VALUES = Relation::VALUE_METHODS -
Relation::CLAUSE_METHODS -
[:includes, :preload, :joins, :left_outer_joins, :order, :reverse_order, :lock, :create_with, :reordering] # :nodoc:
def normal_values
NORMAL_VALUES
end
NORMAL_VALUES = Relation::VALUE_METHODS - Relation::CLAUSE_METHODS -
[
:select, :includes, :preload, :joins, :left_outer_joins,
:order, :reverse_order, :lock, :create_with, :reordering
]
def merge
normal_values.each do |name|
NORMAL_VALUES.each do |name|
value = values[name]
# The unless clause is here mostly for performance reasons (since the `send` call might be moderately
# expensive), most of the time the value is going to be `nil` or `.blank?`, the only catch is that
# `false.blank?` returns `true`, so there needs to be an extra check so that explicit `false` values
# don't fall through the cracks.
unless value.nil? || (value.blank? && false != value)
if name == :select
relation._select!(*value)
else
relation.public_send("#{name}!", *value)
end
relation.public_send(:"#{name}!", *value)
end
end
merge_select_values
merge_multi_values
merge_single_values
merge_clauses
@ -86,6 +81,18 @@ module ActiveRecord
end
private
def merge_select_values
return if other.select_values.empty?
if other.klass == relation.klass
relation.select_values |= other.select_values
else
relation.select_values |= other.instance_eval do
arel_columns(select_values)
end
end
end
def merge_preloads
return if other.preload_values.empty? && other.includes_values.empty?

View File

@ -65,8 +65,7 @@ module ActiveRecord
end
def build_bind_attribute(column_name, value)
attr = Relation::QueryAttribute.new(column_name, value, table.type(column_name))
Arel::Nodes::BindParam.new(attr)
Relation::QueryAttribute.new(column_name, value, table.type(column_name))
end
def resolve_arel_attribute(table_name, column_name, &block)

View File

@ -31,14 +31,10 @@ module ActiveRecord
end
def unboundable?
if defined?(@_unboundable)
@_unboundable
else
value_for_database unless value_before_type_cast.is_a?(StatementCache::Substitute)
@_unboundable = nil
unless defined?(@_unboundable)
@_unboundable = !type.serializable?(value) && type.cast(value) <=> 0
end
rescue ::RangeError
@_unboundable = type.cast(value_before_type_cast) <=> 0
@_unboundable
end
private

View File

@ -1130,10 +1130,9 @@ module ActiveRecord
# scoping.
def excluding(*records)
records.flatten!(1)
records.compact!
raise ArgumentError, "You must pass at least one #{klass.name} object to #excluding." if records.empty?
if records.any? { |record| !record.is_a?(klass) }
unless records.all?(klass)
raise ArgumentError, "You must only pass a single or collection of #{klass.name} objects to #excluding."
end
@ -1263,8 +1262,7 @@ module ActiveRecord
end
def build_cast_value(name, value)
cast_value = ActiveModel::Attribute.with_cast_value(name, value, Type.default_value)
Arel::Nodes::BindParam.new(cast_value)
ActiveModel::Attribute.with_cast_value(name, value, Type.default_value)
end
def build_from

View File

@ -224,11 +224,10 @@ module ActiveRecord
end
def extract_node_value(node)
case node
when Array
node.map { |v| extract_node_value(v) }
when Arel::Nodes::BindParam, Arel::Nodes::Casted, Arel::Nodes::Quoted
if node.respond_to?(:value_before_type_cast)
node.value_before_type_cast
elsif Array === node
node.map { |v| extract_node_value(v) }
end
end
end

View File

@ -4,9 +4,9 @@ module Arel # :nodoc: all
class DeleteManager < Arel::TreeManager
include TreeManager::StatementMethods
def initialize
super
@ast = Nodes::DeleteStatement.new
def initialize(table = nil)
super()
@ast = Nodes::DeleteStatement.new(table)
@ctx = @ast
end

View File

@ -2,9 +2,9 @@
module Arel # :nodoc: all
class InsertManager < Arel::TreeManager
def initialize
super
@ast = Nodes::InsertStatement.new
def initialize(table = nil)
super()
@ast = Nodes::InsertStatement.new(table)
end
def into(table)

View File

@ -47,7 +47,7 @@ module Arel # :nodoc: all
def self.build_quoted(other, attribute = nil)
case other
when Arel::Nodes::Node, Arel::Attributes::Attribute, Arel::Table, Arel::SelectManager, Arel::Nodes::SqlLiteral
when Arel::Nodes::Node, Arel::Attributes::Attribute, Arel::Table, Arel::SelectManager, Arel::Nodes::SqlLiteral, ActiveModel::Attribute
other
else
case attribute

View File

@ -3,17 +3,12 @@
module Arel # :nodoc: all
module Nodes
class DeleteStatement < Arel::Nodes::Node
attr_accessor :left, :right, :orders, :limit, :offset, :key
alias :relation :left
alias :relation= :left=
alias :wheres :right
alias :wheres= :right=
attr_accessor :relation, :wheres, :orders, :limit, :offset, :key
def initialize(relation = nil, wheres = [])
super()
@left = relation
@right = wheres
@relation = relation
@wheres = wheres
@orders = []
@limit = nil
@offset = nil
@ -22,18 +17,18 @@ module Arel # :nodoc: all
def initialize_copy(other)
super
@left = @left.clone if @left
@right = @right.clone if @right
@relation = @relation.clone if @relation
@wheres = @wheres.clone if @wheres
end
def hash
[self.class, @left, @right, @orders, @limit, @offset, @key].hash
[self.class, @relation, @wheres, @orders, @limit, @offset, @key].hash
end
def eql?(other)
self.class == other.class &&
self.left == other.left &&
self.right == other.right &&
self.relation == other.relation &&
self.wheres == other.wheres &&
self.orders == other.orders &&
self.limit == other.limit &&
self.offset == other.offset &&

View File

@ -5,9 +5,9 @@ module Arel # :nodoc: all
class InsertStatement < Arel::Nodes::Node
attr_accessor :relation, :columns, :values, :select
def initialize
def initialize(relation = nil)
super()
@relation = nil
@relation = relation
@columns = []
@values = nil
@select = nil

View File

@ -6,9 +6,9 @@ module Arel # :nodoc: all
attr_accessor :projections, :wheres, :groups, :windows, :comment
attr_accessor :havings, :source, :set_quantifier, :optimizer_hints
def initialize
def initialize(relation = nil)
super()
@source = JoinSource.new nil
@source = JoinSource.new(relation)
# https://ronsavage.github.io/SQL/sql-92.bnf.html#set%20quantifier
@set_quantifier = nil

View File

@ -6,9 +6,9 @@ module Arel # :nodoc: all
attr_reader :cores
attr_accessor :limit, :orders, :lock, :offset, :with
def initialize(cores = [SelectCore.new])
def initialize(relation = nil)
super()
@cores = cores
@cores = [SelectCore.new(relation)]
@orders = []
@limit = nil
@lock = nil

View File

@ -5,8 +5,9 @@ module Arel # :nodoc: all
class UpdateStatement < Arel::Nodes::Node
attr_accessor :relation, :wheres, :values, :orders, :limit, :offset, :key
def initialize
@relation = nil
def initialize(relation = nil)
super()
@relation = relation
@wheres = []
@values = []
@orders = []

View File

@ -52,7 +52,7 @@ module Arel # :nodoc: all
else
left = quoted_node(other.begin)
right = quoted_node(other.end)
Nodes::Between.new(self, left.and(right))
Nodes::Between.new(self, Nodes::And.new([left, right]))
end
end
@ -207,11 +207,11 @@ module Arel # :nodoc: all
end
def contains(other)
Arel::Nodes::Contains.new(self, other)
Arel::Nodes::Contains.new self, quoted_node(other)
end
def overlaps(other)
Arel::Nodes::Overlaps.new(self, other)
Arel::Nodes::Overlaps.new self, quoted_node(other)
end
def quoted_array(others)

View File

@ -8,9 +8,8 @@ module Arel # :nodoc: all
def initialize(table = nil)
super()
@ast = Nodes::SelectStatement.new
@ast = Nodes::SelectStatement.new(table)
@ctx = @ast.cores.last
from table
end
def initialize_copy(other)

View File

@ -2,7 +2,6 @@
module Arel # :nodoc: all
class Table
include Arel::Crud
include Arel::FactoryMethods
include Arel::AliasPredication

View File

@ -4,9 +4,9 @@ module Arel # :nodoc: all
class UpdateManager < Arel::TreeManager
include TreeManager::StatementMethods
def initialize
super
@ast = Nodes::UpdateStatement.new
def initialize(table = nil)
super()
@ast = Nodes::UpdateStatement.new(table)
@ctx = @ast
end

View File

@ -103,7 +103,7 @@ module Arel # :nodoc: all
row.each_with_index do |value, k|
collector << ", " unless k == 0
case value
when Nodes::SqlLiteral, Nodes::BindParam
when Nodes::SqlLiteral, Nodes::BindParam, ActiveModel::Attribute
collector = visit(value, collector)
else
collector << quote(value).to_s
@ -585,7 +585,7 @@ module Arel # :nodoc: all
def visit_Arel_Nodes_Assignment(o, collector)
case o.right
when Arel::Nodes::Node, Arel::Attributes::Attribute
when Arel::Nodes::Node, Arel::Attributes::Attribute, ActiveModel::Attribute
collector = visit o.left, collector
collector << " = "
visit o.right, collector
@ -695,6 +695,10 @@ module Arel # :nodoc: all
def bind_block; BIND_BLOCK; end
def visit_ActiveModel_Attribute(o, collector)
collector.add_bind(o, &bind_block)
end
def visit_Arel_Nodes_BindParam(o, collector)
collector.add_bind(o.value, &bind_block)
end

View File

@ -40,6 +40,9 @@ class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase
expected = %(CREATE INDEX CONCURRENTLY "index_people_on_last_name" ON "people" ("last_name"))
assert_equal expected, add_index(:people, :last_name, algorithm: :concurrently)
expected = %(CREATE INDEX CONCURRENTLY IF NOT EXISTS "index_people_on_last_name" ON "people" ("last_name"))
assert_equal expected, add_index(:people, :last_name, if_not_exists: true, algorithm: :concurrently)
expected = %(CREATE INDEX "index_people_on_last_name_and_first_name" ON "people" ("last_name" DESC, "first_name" ASC))
assert_equal expected, add_index(:people, [:last_name, :first_name], order: { last_name: :desc, first_name: :asc })
assert_equal expected, add_index(:people, ["last_name", :first_name], order: { last_name: :desc, "first_name" => :asc })

View File

@ -1038,15 +1038,13 @@ module Arel
describe "#contains" do
it "should create a Contains node" do
relation = Table.new(:products)
query = Nodes.build_quoted("{foo,bar}")
_(relation[:tags].contains(query)).must_be_kind_of Nodes::Contains
_(relation[:tags].contains(["foo", "bar"])).must_be_kind_of Nodes::Contains
end
it "should generate @> in sql" do
relation = Table.new(:products)
relation = Table.new(:products, type_caster: fake_pg_caster)
mgr = relation.project relation[:id]
query = Nodes.build_quoted("{foo,bar}")
mgr.where relation[:tags].contains(query)
mgr.where relation[:tags].contains(["foo", "bar"])
_(mgr.to_sql).must_be_like %{ SELECT "products"."id" FROM "products" WHERE "products"."tags" @> '{foo,bar}' }
end
end
@ -1054,15 +1052,13 @@ module Arel
describe "#overlaps" do
it "should create an Overlaps node" do
relation = Table.new(:products)
query = Nodes.build_quoted("{foo,bar}")
_(relation[:tags].overlaps(query)).must_be_kind_of Nodes::Overlaps
_(relation[:tags].overlaps(["foo", "bar"])).must_be_kind_of Nodes::Overlaps
end
it "should generate && in sql" do
relation = Table.new(:products)
relation = Table.new(:products, type_caster: fake_pg_caster)
mgr = relation.project relation[:id]
query = Nodes.build_quoted("{foo,bar}")
mgr.where relation[:tags].overlaps(query)
mgr.where relation[:tags].overlaps(["foo", "bar"])
_(mgr.to_sql).must_be_like %{ SELECT "products"."id" FROM "products" WHERE "products"."tags" && '{foo,bar}' }
end
end
@ -1123,6 +1119,19 @@ module Arel
exclude_end?: exclude,
)
end
# Mimic PG::TextDecoder::Array casting
def fake_pg_caster
Object.new.tap do |caster|
def caster.type_cast_for_database(attr_name, value)
if attr_name == "tags"
"{#{value.join(",")}}"
else
value
end
end
end
end
end
end
end

View File

@ -42,13 +42,6 @@ module Arel
assert_equal "bar", join.right
end
it "should return an insert manager" do
im = @relation.compile_insert "VALUES(NULL)"
assert_kind_of Arel::InsertManager, im
im.into Table.new(:users)
assert_equal "INSERT INTO \"users\" VALUES(NULL)", im.to_sql
end
describe "skip" do
it "should add an offset" do
sm = @relation.skip 2

View File

@ -178,15 +178,15 @@ class AsynchronousExecutorTypeTest < ActiveRecord::TestCase
assert async_pool1.is_a?(Concurrent::ThreadPoolExecutor)
assert async_pool2.is_a?(Concurrent::ThreadPoolExecutor)
assert 0, async_pool1.min_length
assert 4, async_pool1.max_length
assert 16, async_pool1.max_queue
assert :caller_runs, async_pool1.fallback_policy
assert_equal 0, async_pool1.min_length
assert_equal 4, async_pool1.max_length
assert_equal 16, async_pool1.max_queue
assert_equal :caller_runs, async_pool1.fallback_policy
assert 0, async_pool2.min_length
assert 4, async_pool2.max_length
assert 16, async_pool2.max_queue
assert :caller_runs, async_pool2.fallback_policy
assert_equal 0, async_pool2.min_length
assert_equal 4, async_pool2.max_length
assert_equal 16, async_pool2.max_queue
assert_equal :caller_runs, async_pool2.fallback_policy
assert_equal 2, handler.all_connection_pools.count
assert_equal async_pool1, async_pool2
@ -199,6 +199,8 @@ class AsynchronousExecutorTypeTest < ActiveRecord::TestCase
old_value = ActiveRecord::Base.async_query_executor
ActiveRecord::Base.async_query_executor = :global_thread_pool
old_concurrency = ActiveRecord::Base.global_executor_concurrency
old_global_thread_pool_async_query_executor = ActiveRecord::Core.class_variable_get(:@@global_thread_pool_async_query_executor)
ActiveRecord::Core.class_variable_set(:@@global_thread_pool_async_query_executor, nil)
ActiveRecord::Base.global_executor_concurrency = 8
handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new
@ -213,15 +215,15 @@ class AsynchronousExecutorTypeTest < ActiveRecord::TestCase
assert async_pool1.is_a?(Concurrent::ThreadPoolExecutor)
assert async_pool2.is_a?(Concurrent::ThreadPoolExecutor)
assert 0, async_pool1.min_length
assert 8, async_pool1.max_length
assert 32, async_pool1.max_queue
assert :caller_runs, async_pool1.fallback_policy
assert_equal 0, async_pool1.min_length
assert_equal 8, async_pool1.max_length
assert_equal 32, async_pool1.max_queue
assert_equal :caller_runs, async_pool1.fallback_policy
assert 0, async_pool2.min_length
assert 8, async_pool2.max_length
assert 32, async_pool2.max_queue
assert :caller_runs, async_pool2.fallback_policy
assert_equal 0, async_pool2.min_length
assert_equal 8, async_pool2.max_length
assert_equal 32, async_pool2.max_queue
assert_equal :caller_runs, async_pool2.fallback_policy
assert_equal 2, handler.all_connection_pools.count
assert_equal async_pool1, async_pool2
@ -229,6 +231,7 @@ class AsynchronousExecutorTypeTest < ActiveRecord::TestCase
clean_up_connection_handler
ActiveRecord::Base.global_executor_concurrency = old_concurrency
ActiveRecord::Base.async_query_executor = old_value
ActiveRecord::Core.class_variable_set(:@@global_thread_pool_async_query_executor, old_global_thread_pool_async_query_executor)
end
def test_concurrency_cannot_be_set_with_null_executor_or_multi_thread_pool
@ -248,7 +251,7 @@ class AsynchronousExecutorTypeTest < ActiveRecord::TestCase
ActiveRecord::Base.async_query_executor = old_value
end
def test_one_global_thread_pool_uses_concurrency_if_set
def test_multi_thread_pool_executor_configuration
old_value = ActiveRecord::Base.async_query_executor
ActiveRecord::Base.async_query_executor = :multi_thread_pool
@ -266,15 +269,50 @@ class AsynchronousExecutorTypeTest < ActiveRecord::TestCase
assert async_pool1.is_a?(Concurrent::ThreadPoolExecutor)
assert async_pool2.is_a?(Concurrent::ThreadPoolExecutor)
assert 0, async_pool1.min_length
assert 10, async_pool1.max_length
assert 40, async_pool1.max_queue
assert :caller_runs, async_pool1.fallback_policy
assert_equal 0, async_pool1.min_length
assert_equal 10, async_pool1.max_length
assert_equal 40, async_pool1.max_queue
assert_equal :caller_runs, async_pool1.fallback_policy
assert 0, async_pool2.min_length
assert 4, async_pool2.max_length
assert 16, async_pool2.max_queue
assert :caller_runs, async_pool2.fallback_policy
assert_equal 0, async_pool2.min_length
assert_equal 5, async_pool2.max_length
assert_equal 20, async_pool2.max_queue
assert_equal :caller_runs, async_pool2.fallback_policy
assert_equal 2, handler.all_connection_pools.count
assert_not_equal async_pool1, async_pool2
ensure
clean_up_connection_handler
ActiveRecord::Base.async_query_executor = old_value
end
def test_multi_thread_pool_is_used_only_by_configurations_that_enable_it
old_value = ActiveRecord::Base.async_query_executor
ActiveRecord::Base.async_query_executor = :multi_thread_pool
handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new
config_hash1 = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary").configuration_hash
new_config1 = config_hash1.merge(min_threads: 0, max_threads: 10)
db_config1 = ActiveRecord::DatabaseConfigurations::HashConfig.new("arunit", "primary", new_config1)
config_hash2 = ActiveRecord::Base.configurations.configs_for(env_name: "arunit2", name: "primary").configuration_hash
new_config2 = config_hash2.merge(min_threads: 0, max_threads: 0)
db_config2 = ActiveRecord::DatabaseConfigurations::HashConfig.new("arunit2", "primary", new_config2)
pool1 = handler.establish_connection(db_config1)
pool2 = handler.establish_connection(db_config2, owner_name: ARUnit2Model)
async_pool1 = pool1.instance_variable_get(:@async_executor)
async_pool2 = pool2.instance_variable_get(:@async_executor)
assert async_pool1.is_a?(Concurrent::ThreadPoolExecutor)
assert_nil async_pool2
assert_equal 0, async_pool1.min_length
assert_equal 10, async_pool1.max_length
assert_equal 40, async_pool1.max_queue
assert_equal :caller_runs, async_pool1.fallback_policy
assert_equal 2, handler.all_connection_pools.count
assert_not_equal async_pool1, async_pool2

View File

@ -430,14 +430,14 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal "a", topic[:title]
end
test "read overriden attribute with predicate respects override" do
test "read overridden attribute with predicate respects override" do
topic = Topic.new
topic.approved = true
def topic.approved; false; end
assert_not topic.approved?, "overriden approved should be false"
assert_not topic.approved?, "overridden approved should be false"
end
test "string attribute predicate" do

View File

@ -1162,15 +1162,15 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal({ "proposed" => 2, "published" => 2 }, Book.group(:status).count)
end
def test_aggregate_attribute_on_custom_type
assert_nil Book.sum(:status)
assert_equal "medium", Book.sum(:difficulty)
assert_equal "easy", Book.minimum(:difficulty)
assert_equal "medium", Book.maximum(:difficulty)
assert_equal({ "proposed" => "proposed", "published" => nil }, Book.group(:status).sum(:status))
assert_equal({ "proposed" => "easy", "published" => "medium" }, Book.group(:status).sum(:difficulty))
assert_equal({ "proposed" => "easy", "published" => "easy" }, Book.group(:status).minimum(:difficulty))
assert_equal({ "proposed" => "easy", "published" => "medium" }, Book.group(:status).maximum(:difficulty))
def test_aggregate_attribute_on_enum_type
assert_equal 4, Book.sum(:status)
assert_equal 1, Book.sum(:difficulty)
assert_equal 0, Book.minimum(:difficulty)
assert_equal 1, Book.maximum(:difficulty)
assert_equal({ "proposed" => 0, "published" => 4 }, Book.group(:status).sum(:status))
assert_equal({ "proposed" => 0, "published" => 1 }, Book.group(:status).sum(:difficulty))
assert_equal({ "proposed" => 0, "published" => 0 }, Book.group(:status).minimum(:difficulty))
assert_equal({ "proposed" => 0, "published" => 1 }, Book.group(:status).maximum(:difficulty))
end
def test_minimum_and_maximum_on_non_numeric_type

View File

@ -47,7 +47,7 @@ module ActiveRecord
assert_equal 1, config.max_threads
end
def test_max_queue_is_pool_multipled_by_4
def test_max_queue_is_pool_multiplied_by_4
config = HashConfig.new("default_env", "primary", {})
assert_equal 5, config.max_threads
assert_equal config.max_threads * 4, config.max_queue

View File

@ -80,6 +80,7 @@ class EnumTest < ActiveRecord::TestCase
assert_not_equal @book, Book.where.not(status: :published).first
assert_equal @book, Book.where.not(status: :written).first
assert_equal books(:ddd), Book.where(last_read: :forgotten).first
assert_nil Book.where(status: :prohibited).first
end
test "find via where with strings" do
@ -90,6 +91,11 @@ class EnumTest < ActiveRecord::TestCase
assert_not_equal @book, Book.where.not(status: "published").first
assert_equal @book, Book.where.not(status: "written").first
assert_equal books(:ddd), Book.where(last_read: "forgotten").first
assert_nil Book.where(status: "prohibited").first
end
test "find via where with large number" do
assert_equal @book, Book.where(status: [2, 9223372036854775808]).first
end
test "find via where should be type casted" do

View File

@ -7,71 +7,52 @@ require "models/comment"
class ExcludingTest < ActiveRecord::TestCase
fixtures :posts, :comments
def test_result_set_does_not_include_single_excluded_record
post = posts(:welcome)
setup { @post = posts(:welcome) }
assert_not_includes Post.excluding(post).to_a, post
def test_result_set_does_not_include_single_excluded_record
assert_not_includes Post.excluding(@post), @post
assert_not_includes Post.excluding(@post).to_a, @post
assert_not_includes Post.without(@post), @post
end
def test_result_set_does_not_include_collection_of_excluded_records
post_welcome = posts(:welcome)
post_thinking = posts(:thinking)
relation = Post.excluding(post_welcome, post_thinking)
assert_not_includes relation.to_a, post_welcome
assert_not_includes relation.to_a, post_thinking
relation = Post.excluding(@post, posts(:thinking))
assert_not_includes relation, @post
assert_not_includes relation, posts(:thinking)
end
def test_result_set_through_association_does_not_include_single_excluded_record
post = posts(:welcome)
comment_greetings = comments(:greetings)
comment_more_greetings = comments(:more_greetings)
comment_greetings, comment_more_greetings = comments(:greetings, :more_greetings)
relation = post.comments.excluding(comment_greetings)
assert_not_includes relation.to_a, comment_greetings
assert_includes relation.to_a, comment_more_greetings
relation = @post.comments.excluding(comment_greetings)
assert_not_includes relation, comment_greetings
assert_includes relation, comment_more_greetings
end
def test_result_set_through_association_does_not_include_collection_of_excluded_records
post = posts(:welcome)
comment_greetings = comments(:greetings)
comment_more_greetings = comments(:more_greetings)
comment_greetings, comment_more_greetings = comments(:greetings, :more_greetings)
relation = post.comments.excluding([comment_greetings, comment_more_greetings])
assert_not_includes relation.to_a, comment_greetings
assert_not_includes relation.to_a, comment_more_greetings
relation = @post.comments.excluding([ comment_greetings, comment_more_greetings ])
assert_not_includes relation, comment_greetings
assert_not_includes relation, comment_more_greetings
end
def test_raises_on_no_arguments
exception = assert_raises ArgumentError do
Post.excluding()
end
assert_equal "You must pass at least one Post object to #excluding.", exception.message
end
def test_raises_on_empty_collection_argument
exception = assert_raises ArgumentError do
Post.excluding([])
end
assert_equal "You must pass at least one Post object to #excluding.", exception.message
def test_does_not_exclude_records_when_no_arguments
assert_no_excludes Post.excluding
assert_no_excludes Post.excluding(nil)
assert_no_excludes Post.excluding([])
assert_no_excludes Post.excluding([ nil ])
end
def test_raises_on_record_from_different_class
post = posts(:welcome)
comment = comments(:greetings)
error = assert_raises(ArgumentError) { Post.excluding(@post, comments(:greetings)) }
assert_equal "You must only pass a single or collection of Post objects to #excluding.", error.message
end
exception = assert_raises ArgumentError do
Post.excluding(post, comment)
private
def assert_no_excludes(relation)
assert_includes relation, @post
assert_equal Post.count, relation.count
end
assert_equal "You must only pass a single or collection of Post objects to #excluding.", exception.message
end
def test_result_set_does_not_include_without_record
post = posts(:welcome)
assert_not_includes Post.without(post).to_a, post
end
end

View File

@ -3,6 +3,7 @@
require "cases/helper"
require "models/post"
require "models/comment"
require "models/other_dog"
module ActiveRecord
class LoadAsyncTest < ActiveRecord::TestCase
@ -280,5 +281,234 @@ module ActiveRecord
assert_predicate defered_posts, :loaded?
end
end
class LoadAsyncMultiThreadPoolExecutorTest < ActiveRecord::TestCase
self.use_transactional_tests = false
fixtures :posts, :comments
def setup
@old_config = ActiveRecord::Base.async_query_executor
ActiveRecord::Base.async_query_executor = :multi_thread_pool
handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new
config_hash1 = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary").configuration_hash
new_config1 = config_hash1.merge(min_threads: 0, max_threads: 10)
db_config1 = ActiveRecord::DatabaseConfigurations::HashConfig.new("arunit", "primary", new_config1)
config_hash2 = ActiveRecord::Base.configurations.configs_for(env_name: "arunit2", name: "primary").configuration_hash
new_config2 = config_hash2.merge(min_threads: 0, max_threads: 10)
db_config2 = ActiveRecord::DatabaseConfigurations::HashConfig.new("arunit2", "primary", new_config2)
handler.establish_connection(db_config1)
handler.establish_connection(db_config2, owner_name: ARUnit2Model)
end
def teardown
ActiveRecord::Base.async_query_executor = @old_config
clean_up_connection_handler
end
def test_scheduled?
defered_posts = Post.where(author_id: 1).load_async
assert_predicate defered_posts, :scheduled?
assert_predicate defered_posts, :loaded?
defered_posts.to_a
assert_not_predicate defered_posts, :scheduled?
end
def test_reset
defered_posts = Post.where(author_id: 1).load_async
assert_predicate defered_posts, :scheduled?
defered_posts.reset
assert_not_predicate defered_posts, :scheduled?
end
def test_simple_query
expected_records = Post.where(author_id: 1).to_a
status = {}
monitor = Monitor.new
condition = monitor.new_cond
subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |event|
if event.payload[:name] == "Post Load"
status[:executed] = true
status[:async] = event.payload[:async]
monitor.synchronize { condition.signal }
end
end
defered_posts = Post.where(author_id: 1).load_async
monitor.synchronize do
condition.wait_until { status[:executed] }
end
assert_equal expected_records, defered_posts.to_a
assert_equal Post.connection.supports_concurrent_connections?, status[:async]
ensure
ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
end
def test_load_async_from_transaction
posts = nil
Post.transaction do
Post.where(author_id: 1).update_all(title: "In Transaction")
posts = Post.where(author_id: 1).load_async
assert_predicate posts, :scheduled?
assert_predicate posts, :loaded?
raise ActiveRecord::Rollback
end
assert_not_nil posts
assert_equal ["In Transaction"], posts.map(&:title).uniq
end
def test_eager_loading_query
expected_records = Post.where(author_id: 1).eager_load(:comments).to_a
status = {}
monitor = Monitor.new
condition = monitor.new_cond
subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |event|
if event.payload[:name] == "SQL"
status[:executed] = true
status[:async] = event.payload[:async]
monitor.synchronize { condition.signal }
end
end
defered_posts = Post.where(author_id: 1).eager_load(:comments).load_async
assert_predicate defered_posts, :scheduled?
monitor.synchronize do
condition.wait_until { status[:executed] }
end
assert_equal expected_records, defered_posts.to_a
assert_queries(0) do
defered_posts.each(&:comments)
end
assert_equal Post.connection.supports_concurrent_connections?, status[:async]
ensure
ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
end
def test_contradiction
assert_queries(0) do
assert_equal [], Post.where(id: []).load_async.to_a
end
Post.where(id: []).load_async.reset
end
def test_pluck
titles = Post.where(author_id: 1).pluck(:title)
assert_equal titles, Post.where(author_id: 1).load_async.pluck(:title)
end
def test_size
expected_size = Post.where(author_id: 1).size
defered_posts = Post.where(author_id: 1).load_async
assert_equal expected_size, defered_posts.size
assert_predicate defered_posts, :loaded?
end
def test_empty?
defered_posts = Post.where(author_id: 1).load_async
assert_equal false, defered_posts.empty?
assert_predicate defered_posts, :loaded?
end
end
class LoadAsyncMixedThreadPoolExecutorTest < ActiveRecord::TestCase
self.use_transactional_tests = false
fixtures :posts, :comments, :other_dogs
def setup
@previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env"
@old_config = ActiveRecord::Base.async_query_executor
ActiveRecord::Base.async_query_executor = :multi_thread_pool
config_hash1 = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary").configuration_hash
config_hash2 = ActiveRecord::Base.configurations.configs_for(env_name: "arunit2", name: "primary").configuration_hash
config = {
"default_env" => {
"animals" => config_hash2.merge({ min_threads: 0, max_threads: 0 }),
"primary" => config_hash1.merge({ min_threads: 0, max_threads: 10 })
}
}
@prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config
ActiveRecord::Base.establish_connection(:primary)
ARUnit2Model.establish_connection(:animals)
end
def teardown
ENV["RAILS_ENV"] = @previous_env
ActiveRecord::Base.configurations = @prev_configs
ActiveRecord::Base.async_query_executor = @old_config
clean_up_connection_handler
end
def test_scheduled?
defered_posts = Post.where(author_id: 1).load_async
assert_predicate defered_posts, :scheduled?
assert_predicate defered_posts, :loaded?
defered_posts.to_a
assert_not_predicate defered_posts, :scheduled?
defered_dogs = OtherDog.where(id: 1).load_async
assert_not_predicate defered_dogs, :scheduled?
assert_predicate defered_dogs, :loaded?
end
def test_simple_query
expected_records = Post.where(author_id: 1).to_a
expected_dogs = OtherDog.where(id: 1).to_a
status = {}
dog_status = {}
monitor = Monitor.new
condition = monitor.new_cond
subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |event|
if event.payload[:name] == "Post Load"
status[:executed] = true
status[:async] = event.payload[:async]
monitor.synchronize { condition.signal }
end
if event.payload[:name] == "OtherDog Load"
dog_status[:executed] = true
dog_status[:async] = event.payload[:async]
monitor.synchronize { condition.signal }
end
end
defered_posts = Post.where(author_id: 1).load_async
defered_dogs = OtherDog.where(id: 1).load_async
monitor.synchronize do
condition.wait_until { status[:executed] }
condition.wait_until { dog_status[:executed] }
end
assert_equal expected_records, defered_posts.to_a
assert_equal expected_dogs, defered_dogs.to_a
assert_equal Post.connection.async_enabled?, status[:async]
assert_equal OtherDog.connection.async_enabled?, dog_status[:async]
ensure
ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
end
end
end
end

View File

@ -41,6 +41,20 @@ module ActiveRecord
end
private :assert_non_select_columns_wont_be_loaded
def test_merging_select_from_different_model
posts = Post.select(:id, :title).joins(:comments)
comments = Comment.where(body: "Thank you for the welcome")
[
posts.merge(comments.select(:body)).first,
posts.merge(comments.select("comments.body")).first,
].each do |post|
assert_equal 1, post.id
assert_equal "Welcome to the weblog", post.title
assert_equal "Thank you for the welcome", post.body
end
end
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

View File

@ -102,7 +102,7 @@ class NumericalityValidationTest < ActiveRecord::TestCase
if RUBY_VERSION > "3.0.0"
# BigDecimal's to_d behavior changed in BigDecimal 3.0.1, see https://github.com/ruby/bigdecimal/issues/70
# TOOD: replace this with a check against BigDecimal::VERSION
# TODO: replace this with a check against BigDecimal::VERSION
assert_not_predicate subject, :valid?
else
assert_predicate subject, :valid?

View File

@ -32,7 +32,7 @@ class User < ApplicationRecord
end
# Attach an avatar to the user.
user.avatar.attach(io: File.open("/path/to/face.jpg"), filename: "face.jpg", content_type: "image/jpg")
user.avatar.attach(io: File.open("/path/to/face.jpg"), filename: "face.jpg", content_type: "image/jpeg")
# Does the user have an avatar?
user.avatar.attached? # => true

View File

@ -25,7 +25,7 @@ module ActiveStorage
#
# document.images.attach(params[:images]) # Array of ActionDispatch::Http::UploadedFile objects
# document.images.attach(params[:signed_blob_id]) # Signed reference to blob from direct upload
# document.images.attach(io: File.open("/path/to/racecar.jpg"), filename: "racecar.jpg", content_type: "image/jpg")
# document.images.attach(io: File.open("/path/to/racecar.jpg"), filename: "racecar.jpg", content_type: "image/jpeg")
# document.images.attach([ first_blob, second_blob ])
def attach(*attachables)
if record.persisted? && !record.changed?

View File

@ -25,7 +25,7 @@ module ActiveStorage
#
# person.avatar.attach(params[:avatar]) # ActionDispatch::Http::UploadedFile object
# person.avatar.attach(params[:signed_blob_id]) # Signed reference to blob from direct upload
# person.avatar.attach(io: File.open("/path/to/face.jpg"), filename: "face.jpg", content_type: "image/jpg")
# person.avatar.attach(io: File.open("/path/to/face.jpg"), filename: "face.jpg", content_type: "image/jpeg")
# person.avatar.attach(avatar_blob) # ActiveStorage::Blob object
def attach(attachable)
if record.persisted? && !record.changed?

View File

@ -5,12 +5,12 @@ require "database/setup"
class ActiveStorage::DiskControllerTest < ActionDispatch::IntegrationTest
test "showing blob inline" do
blob = create_blob(filename: "hello.jpg", content_type: "image/jpg")
blob = create_blob(filename: "hello.jpg", content_type: "image/jpeg")
get blob.url
assert_response :ok
assert_equal "inline; filename=\"hello.jpg\"; filename*=UTF-8''hello.jpg", response.headers["Content-Disposition"]
assert_equal "image/jpg", response.headers["Content-Type"]
assert_equal "image/jpeg", response.headers["Content-Type"]
assert_equal "Hello world!", response.body
end
@ -46,11 +46,11 @@ class ActiveStorage::DiskControllerTest < ActionDispatch::IntegrationTest
test "showing public blob" do
with_service("local_public") do
blob = create_blob(content_type: "image/jpg")
blob = create_blob(content_type: "image/jpeg")
get blob.url
assert_response :ok
assert_equal "image/jpg", response.headers["Content-Type"]
assert_equal "image/jpeg", response.headers["Content-Type"]
assert_equal "Hello world!", response.body
end
end

View File

@ -31,7 +31,7 @@ class ActiveStorage::ManyAttachedTest < ActiveSupport::TestCase
test "attaching new blobs from Hashes to an existing record" do
@user.highlights.attach(
{ io: StringIO.new("STUFF"), filename: "funky.jpg", content_type: "image/jpg" },
{ io: StringIO.new("STUFF"), filename: "funky.jpg", content_type: "image/jpeg" },
{ io: StringIO.new("THINGS"), filename: "town.jpg", content_type: "image/jpeg" })
assert_equal "funky.jpg", @user.highlights.first.filename.to_s
@ -81,7 +81,7 @@ class ActiveStorage::ManyAttachedTest < ActiveSupport::TestCase
assert @user.changed?
@user.highlights.attach(
{ io: StringIO.new("STUFF"), filename: "funky.jpg", content_type: "image/jpg" },
{ io: StringIO.new("STUFF"), filename: "funky.jpg", content_type: "image/jpeg" },
{ io: StringIO.new("THINGS"), filename: "town.jpg", content_type: "image/jpeg" })
assert_equal "funky.jpg", @user.highlights.first.filename.to_s
@ -337,8 +337,8 @@ class ActiveStorage::ManyAttachedTest < ActiveSupport::TestCase
test "attaching new blobs from Hashes to a new record" do
User.new(name: "Jason").tap do |user|
user.highlights.attach(
{ io: StringIO.new("STUFF"), filename: "funky.jpg", content_type: "image/jpg" },
{ io: StringIO.new("THINGS"), filename: "town.jpg", content_type: "image/jpg" })
{ io: StringIO.new("STUFF"), filename: "funky.jpg", content_type: "image/jpeg" },
{ io: StringIO.new("THINGS"), filename: "town.jpg", content_type: "image/jpeg" })
assert user.new_record?
assert user.highlights.first.new_record?
@ -600,8 +600,8 @@ class ActiveStorage::ManyAttachedTest < ActiveSupport::TestCase
test "attaching a new blob from a Hash with a custom service" do
with_service("mirror") do
@user.highlights.attach io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg"
@user.vlogs.attach io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg"
@user.highlights.attach io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpeg"
@user.vlogs.attach io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpeg"
assert_instance_of ActiveStorage::Service::MirrorService, @user.highlights.first.service
assert_instance_of ActiveStorage::Service::DiskService, @user.vlogs.first.service

View File

@ -38,12 +38,12 @@ class ActiveStorage::OneAttachedTest < ActiveSupport::TestCase
end
test "attaching a new blob from a Hash to an existing record" do
@user.avatar.attach io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg"
@user.avatar.attach io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpeg"
assert_equal "town.jpg", @user.avatar.filename.to_s
end
test "attaching a new blob from a Hash to an existing record passes record" do
hash = { io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" }
hash = { io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpeg" }
blob = ActiveStorage::Blob.build_after_unfurling(**hash)
arguments = [hash.merge(record: @user, service_name: nil)]
assert_called_with(ActiveStorage::Blob, :build_after_unfurling, arguments, returns: blob) do
@ -98,7 +98,7 @@ class ActiveStorage::OneAttachedTest < ActiveSupport::TestCase
@user.name = "Tina"
assert @user.changed?
@user.avatar.attach io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg"
@user.avatar.attach io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpeg"
assert_equal "town.jpg", @user.avatar.filename.to_s
assert_not @user.avatar.persisted?
assert @user.will_save_change_to_name?
@ -320,7 +320,7 @@ class ActiveStorage::OneAttachedTest < ActiveSupport::TestCase
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 = Group.create!(users_attributes: [{ name: "John", avatar: { io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpeg" } }])
group.save!
new_user = User.find_by(name: "John")
assert new_user.avatar.attached?
@ -358,7 +358,7 @@ class ActiveStorage::OneAttachedTest < ActiveSupport::TestCase
test "attaching a new blob from a Hash to a new record" do
User.new(name: "Jason").tap do |user|
user.avatar.attach io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg"
user.avatar.attach io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpeg"
assert user.new_record?
assert user.avatar.attachment.new_record?
assert user.avatar.blob.new_record?
@ -582,8 +582,8 @@ class ActiveStorage::OneAttachedTest < ActiveSupport::TestCase
test "attaching a new blob from a Hash with a custom service" do
with_service("mirror") do
@user.avatar.attach io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg"
@user.cover_photo.attach io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg"
@user.avatar.attach io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpeg"
@user.cover_photo.attach io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpeg"
assert_instance_of ActiveStorage::Service::MirrorService, @user.avatar.service
assert_instance_of ActiveStorage::Service::DiskService, @user.cover_photo.service

View File

@ -60,16 +60,6 @@ class ActiveStorage::VariantTest < ActiveSupport::TestCase
assert_match(/RGB/, image.colorspace)
end
test "variation with :combine_options is not supported" do
blob = create_file_blob(filename: "racecar.jpg")
assert_raises(ArgumentError) do
blob.variant(combine_options: {
resize: "100x100",
monochrome: false
}).processed
end
end
test "center-weighted crop of JPEG blob using :resize_to_fill" do
blob = create_file_blob(filename: "racecar.jpg")
variant = blob.variant(resize_to_fill: [100, 100]).processed

View File

@ -50,7 +50,7 @@ module ActiveStorage::Service::SharedServiceTests
StringIO.new(data),
checksum: Digest::MD5.base64digest(data),
filename: "racecar.jpg",
content_type: "image/jpg"
content_type: "image/jpeg"
)
assert_equal data, @service.download(key)

View File

@ -289,21 +289,17 @@ module ActiveSupport
end
attr_accessor :kind, :name
attr_reader :chain_config
attr_reader :chain_config, :filter
def initialize(name, filter, kind, options, chain_config)
@chain_config = chain_config
@name = name
@kind = kind
@filter = filter
@key = compute_identifier filter
@if = check_conditionals(options[:if])
@unless = check_conditionals(options[:unless])
end
def filter; @key; end
def raw_filter; @filter; end
def merge_conditional_options(chain, if_option:, unless_option:)
options = {
if: @if.dup,
@ -367,15 +363,6 @@ module ActiveSupport
conditionals.freeze
end
def compute_identifier(filter)
case filter
when ::Proc
filter.object_id
else
filter
end
end
def conditions_lambdas
@if.map { |c| CallTemplate.build(c, self).make_lambda } +
@unless.map { |c| CallTemplate.build(c, self).inverted_lambda }

View File

@ -2,6 +2,7 @@
require "active_support/callbacks"
require "active_support/core_ext/enumerable"
require "active_support/core_ext/module/delegation"
module ActiveSupport
# Abstract super class that provides a thread-isolated attributes singleton, which resets automatically
@ -155,14 +156,15 @@ module ActiveSupport
@current_instances_key ||= name.to_sym
end
def method_missing(name, *args, **kwargs, &block)
def method_missing(name, *args, &block)
# Caches the method definition as a singleton method of the receiver.
#
# By letting #delegate handle it, we avoid an enclosure that'll capture args.
singleton_class.delegate name, to: :instance
send(name, *args, **kwargs, &block)
send(name, *args, &block)
end
ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
def respond_to_missing?(name, _)
super || instance.respond_to?(name)

View File

@ -86,12 +86,6 @@ module ActiveSupport #:nodoc:
# to allow arbitrary constants to be marked for unloading.
mattr_accessor :explicitly_unloadable_constants, default: []
# The logger used when tracing autoloads.
mattr_accessor :logger
# If true, trace autoloads with +logger.debug+.
mattr_accessor :verbose, default: false
# The WatchStack keeps a stack of the modules being watched as files are
# loaded. If a file in the process of being loaded (parent.rb) triggers the
# load of another file (child.rb) the stack will ensure that child.rb
@ -298,9 +292,6 @@ module ActiveSupport #:nodoc:
else
yield
end
rescue Exception => exception # errors from loading file
exception.blame_file! file if exception.respond_to? :blame_file!
raise
end
# Mark the given constant as unloadable. Unloadable constants are removed
@ -334,31 +325,9 @@ module ActiveSupport #:nodoc:
end
end
# Exception file-blaming.
module Blamable #:nodoc:
def blame_file!(file)
(@blamed_files ||= []).unshift file
end
def blamed_files
@blamed_files ||= []
end
def describe_blame
return nil if blamed_files.empty?
"This error occurred while loading the following files:\n #{blamed_files.join "\n "}"
end
def copy_blame!(exc)
@blamed_files = exc.blamed_files.clone
self
end
end
def hook!
Loadable.include_into(Object)
ModuleConstMissing.include_into(Module)
Exception.include(Blamable)
end
def unhook!
@ -381,7 +350,6 @@ module ActiveSupport #:nodoc:
load_error.message
end
load_error_message.replace(message % file_name)
load_error.copy_blame!(load_error)
end
raise
end
@ -500,7 +468,6 @@ module ActiveSupport #:nodoc:
return nil unless base_path = autoloadable_module?(path_suffix)
mod = Module.new
into.const_set const_name, mod
log("constant #{qualified_name} autoloaded (module autovivified from #{File.join(base_path, path_suffix)})")
autoloaded_constants << qualified_name unless autoload_once_paths.include?(base_path)
autoloaded_constants.uniq!
mod
@ -558,7 +525,6 @@ module ActiveSupport #:nodoc:
require_or_load(expanded, qualified_name)
if from_mod.const_defined?(const_name, false)
log("constant #{qualified_name} autoloaded from #{expanded}.rb")
return from_mod.const_get(const_name)
else
raise LoadError, "Unable to autoload constant #{qualified_name}, expected #{file_path} to define it"
@ -611,66 +577,21 @@ module ActiveSupport #:nodoc:
# as the environment will be in an inconsistent state, e.g. other constants
# may have already been unloaded and not accessible.
def remove_unloadable_constants!
log("removing unloadable constants")
autoloaded_constants.each { |const| remove_constant const }
autoloaded_constants.clear
Reference.clear!
explicitly_unloadable_constants.each { |const| remove_constant const }
end
class ClassCache
def initialize
@store = Concurrent::Map.new
end
def empty?
@store.empty?
end
def key?(key)
@store.key?(key)
end
def get(key)
key = key.name if key.respond_to?(:name)
@store[key] ||= Inflector.constantize(key)
end
alias :[] :get
def safe_get(key)
key = key.name if key.respond_to?(:name)
@store[key] ||= Inflector.safe_constantize(key)
end
def store(klass)
return self unless klass.respond_to?(:name)
raise(ArgumentError, "anonymous classes cannot be cached") if klass.name.empty?
@store[klass.name] = klass
self
end
def clear!
@store.clear
end
end
Reference = ClassCache.new
# Store a reference to a class +klass+.
def reference(klass)
Reference.store klass
end
# Get the reference for class named +name+.
# Raises an exception if referenced class does not exist.
def constantize(name)
Reference.get(name)
Inflector.constantize(name)
end
# Get the reference for class named +name+ if one exists.
# Otherwise returns +nil+.
def safe_constantize(name)
Reference.safe_get(name)
Inflector.safe_constantize(name)
end
# Determine if the given constant has been automatically loaded.
@ -802,10 +723,6 @@ module ActiveSupport #:nodoc:
end
end
def log(message)
logger.debug("autoloading: #{message}") if logger && verbose
end
private
def uninitialized_constant(qualified_name, const_name, receiver:)
NameError.new("uninitialized constant #{qualified_name}", const_name, receiver: receiver)

View File

@ -2,6 +2,7 @@
require "set"
require "active_support/core_ext/string/inflections"
require "zeitwerk"
module ActiveSupport
module Dependencies

View File

@ -1129,15 +1129,6 @@ module CallbacksTest
}
end
def test_skip_lambda # raises error
calls = []
callback = ->(o) { calls << o }
klass = build_class(callback)
assert_raises(ArgumentError) { klass.skip callback }
klass.new.run
assert_equal 10, calls.length
end
def test_skip_symbol # removes all
calls = []
klass = build_class(:bar)

View File

@ -1,80 +0,0 @@
# frozen_string_literal: true
require_relative "abstract_unit"
require "active_support/dependencies"
module ActiveSupport
module Dependencies
class ClassCacheTest < ActiveSupport::TestCase
def setup
@cache = ClassCache.new
end
def test_empty?
assert_empty @cache
@cache.store(ClassCacheTest)
assert_not_empty @cache
end
def test_clear!
assert_empty @cache
@cache.store(ClassCacheTest)
assert_not_empty @cache
@cache.clear!
assert_empty @cache
end
def test_set_key
@cache.store(ClassCacheTest)
assert @cache.key?(ClassCacheTest.name)
end
def test_get_with_class
@cache.store(ClassCacheTest)
assert_equal ClassCacheTest, @cache.get(ClassCacheTest)
end
def test_get_with_name
@cache.store(ClassCacheTest)
assert_equal ClassCacheTest, @cache.get(ClassCacheTest.name)
end
def test_get_constantizes
assert_empty @cache
assert_equal ClassCacheTest, @cache.get(ClassCacheTest.name)
end
def test_get_constantizes_fails_on_invalid_names
assert_empty @cache
assert_raise NameError do
@cache.get("OmgTotallyInvalidConstantName")
end
end
def test_get_alias
assert_empty @cache
assert_equal @cache[ClassCacheTest.name], @cache.get(ClassCacheTest.name)
end
def test_safe_get_constantizes
assert_empty @cache
assert_equal ClassCacheTest, @cache.safe_get(ClassCacheTest.name)
end
def test_safe_get_constantizes_doesnt_fail_on_invalid_names
assert_empty @cache
assert_nil @cache.safe_get("OmgTotallyInvalidConstantName")
end
def test_new_rejects_strings
@cache.store ClassCacheTest.name
assert_not @cache.key?(ClassCacheTest.name)
end
def test_store_returns_self
x = @cache.store ClassCacheTest
assert_equal @cache, x
end
end
end
end

View File

@ -36,6 +36,12 @@ class CurrentAttributesTest < ActiveSupport::TestCase
self.account = account
end
def get_world_and_account(hash)
hash[:world] = world
hash[:account] = account
hash
end
def respond_to_test; end
def request
@ -138,6 +144,11 @@ class CurrentAttributesTest < ActiveSupport::TestCase
assert_equal "world/1", Current.world
assert_equal "account/1", Current.account
hash = {}
assert_same hash, Current.get_world_and_account(hash)
assert_equal "world/1", hash[:world]
assert_equal "account/1", hash[:account]
end
setup { @testing_teardown = false }

View File

@ -1,7 +0,0 @@
# frozen_string_literal: true
exception = Exception.new("I am not blamable!")
class << exception
undef_method(:blame_file!)
end
raise exception

View File

@ -104,13 +104,6 @@ class DependenciesTest < ActiveSupport::TestCase
end
end
def test_dependency_which_raises_doesnt_blindly_call_blame_file!
with_loading do
filename = "dependencies/raises_exception_without_blame_file"
assert_raises(Exception) { require_dependency filename }
end
end
def test_warnings_should_be_enabled_on_first_load
with_loading "dependencies" do
old_warnings, ActiveSupport::Dependencies.warnings_on_first_load = ActiveSupport::Dependencies.warnings_on_first_load, true
@ -708,21 +701,6 @@ class DependenciesTest < ActiveSupport::TestCase
remove_constants(:ServiceOne)
end
def test_references_should_work
with_loading "dependencies" do
c = ActiveSupport::Dependencies.reference("ServiceOne")
service_one_first = ServiceOne
assert_equal service_one_first, c.get("ServiceOne")
ActiveSupport::Dependencies.clear
assert_not defined?(ServiceOne)
service_one_second = ServiceOne
assert_not_equal service_one_first, c.get("ServiceOne")
assert_equal service_one_second, c.get("ServiceOne")
end
ensure
remove_constants(:ServiceOne)
end
def test_constantize_shortcut_for_cached_constant_lookups
with_loading "dependencies" do
assert_equal ServiceOne, ActiveSupport::Dependencies.constantize("ServiceOne")
@ -1169,52 +1147,3 @@ class DependenciesTest < ActiveSupport::TestCase
ActiveSupport::Dependencies.hook!
end
end
class DependenciesLogging < ActiveSupport::TestCase
MESSAGE = "message"
def with_settings(logger, verbose)
original_logger = ActiveSupport::Dependencies.logger
original_verbose = ActiveSupport::Dependencies.verbose
ActiveSupport::Dependencies.logger = logger
ActiveSupport::Dependencies.verbose = verbose
yield
ensure
ActiveSupport::Dependencies.logger = original_logger
ActiveSupport::Dependencies.verbose = original_verbose
end
def fake_logger
Class.new do
def self.debug(message)
message
end
end
end
test "does not log if the logger is nil and verbose is false" do
with_settings(nil, false) do
assert_nil ActiveSupport::Dependencies.log(MESSAGE)
end
end
test "does not log if the logger is nil and verbose is true" do
with_settings(nil, true) do
assert_nil ActiveSupport::Dependencies.log(MESSAGE)
end
end
test "does not log if the logger is set and verbose is false" do
with_settings(fake_logger, false) do
assert_nil ActiveSupport::Dependencies.log(MESSAGE)
end
end
test "logs if the logger is set and verbose is true" do
with_settings(fake_logger, true) do
assert_equal "autoloading: #{MESSAGE}", ActiveSupport::Dependencies.log(MESSAGE)
end
end
end

View File

@ -325,9 +325,9 @@ class SetupAndTeardownTest < ActiveSupport::TestCase
teardown :foo, :sentinel
def test_inherited_setup_callbacks
assert_equal [:reset_callback_record, :foo], self.class._setup_callbacks.map(&:raw_filter)
assert_equal [:reset_callback_record, :foo], self.class._setup_callbacks.map(&:filter)
assert_equal [:foo], @called_back
assert_equal [:foo, :sentinel], self.class._teardown_callbacks.map(&:raw_filter)
assert_equal [:foo, :sentinel], self.class._teardown_callbacks.map(&:filter)
end
def setup
@ -355,9 +355,9 @@ class SubclassSetupAndTeardownTest < SetupAndTeardownTest
teardown :bar
def test_inherited_setup_callbacks
assert_equal [:reset_callback_record, :foo, :bar], self.class._setup_callbacks.map(&:raw_filter)
assert_equal [:reset_callback_record, :foo, :bar], self.class._setup_callbacks.map(&:filter)
assert_equal [:foo, :bar], @called_back
assert_equal [:foo, :sentinel, :bar], self.class._teardown_callbacks.map(&:raw_filter)
assert_equal [:foo, :sentinel, :bar], self.class._teardown_callbacks.map(&:filter)
end
private

View File

@ -347,8 +347,8 @@ number_to_human(1234567) # => 1.23 Million
Formats the bytes in size into a more understandable representation; useful for reporting file sizes to users.
```ruby
number_to_human_size(1234) # => 1.2 KB
number_to_human_size(1234567) # => 1.2 MB
number_to_human_size(1234) # => 1.21 KB
number_to_human_size(1234567) # => 1.18 MB
```
#### number_to_percentage

View File

@ -50,7 +50,7 @@ The `database.yml` looks like this:
production:
database: my_primary_database
user: root
adapter: mysql
adapter: mysql2
```
Let's add a replica for the first configuration, and a second database called animals and a
@ -68,21 +68,21 @@ production:
primary:
database: my_primary_database
user: root
adapter: mysql
adapter: mysql2
primary_replica:
database: my_primary_database
user: root_readonly
adapter: mysql
adapter: mysql2
replica: true
animals:
database: my_animals_database
user: animals_root
adapter: mysql
adapter: mysql2
migrations_paths: db/animals_migrate
animals_replica:
database: my_animals_database
user: animals_readonly
adapter: mysql
adapter: mysql2
replica: true
```
@ -338,17 +338,17 @@ Shards are declared in the three-tier config like this:
production:
primary:
database: my_primary_database
adapter: mysql
adapter: mysql2
primary_replica:
database: my_primary_database
adapter: mysql
adapter: mysql2
replica: true
primary_shard_one:
database: my_primary_shard_one
adapter: mysql
adapter: mysql2
primary_shard_one_replica:
database: my_primary_shard_one
adapter: mysql
adapter: mysql2
replica: true
```

View File

@ -89,7 +89,7 @@ INFO. Autoload paths are called _root directories_ in Zeitwerk documentation, bu
Within an autoload path, file names must match the constants they define as documented [here](https://github.com/fxn/zeitwerk#file-structure).
By default, the autoload paths of an application consist of all the subdirectories of `app` that exist when the application boots ---except for `assets`, `javascript`, `views`,--- plus the autoload paths of engines it might depend on.
By default, the autoload paths of an application consist of all the subdirectories of `app` that exist when the application boots ---except for `assets`, `javascript`, and `views`--- plus the autoload paths of engines it might depend on.
For example, if `UsersHelper` is implemented in `app/helpers/users_helper.rb`, the module is autoloadable, you do not need (and should not write) a `require` call for it:
@ -98,17 +98,17 @@ $ bin/rails runner 'p UsersHelper'
UsersHelper
```
Autoload paths automatically pick any custom directories under `app`. For example, if your application has `app/presenters`, or `app/services`, etc., they are added to autoload paths.
Autoload paths automatically pick up any custom directories under `app`. For example, if your application has `app/presenters`, or `app/services`, etc., they are added to autoload paths.
The array of autoload paths can be extended by mutating `config.autoload_paths`, in `config/application.rb`, but nowadays this is discouraged.
WARNING. Please, do not mutate `ActiveSupport::Dependencies.autoload_paths`, the public interface to change autoload paths is `config.autoload_paths`.
WARNING. Please do not mutate `ActiveSupport::Dependencies.autoload_paths`; the public interface to change autoload paths is `config.autoload_paths`.
$LOAD_PATH
----------
Autoload paths are added to `$LOAD_PATH` by default. However, Zeitwerk uses absolute file names internally, and your application should not issue `require` calls for autoloadable files, so those directories are actually not needed there. You can opt-out with this flag:
Autoload paths are added to `$LOAD_PATH` by default. However, Zeitwerk uses absolute file names internally, and your application should not issue `require` calls for autoloadable files, so those directories are actually not needed there. You can opt out with this flag:
```ruby
config.add_autoload_paths_to_load_path = false
@ -122,13 +122,13 @@ Reloading
Rails automatically reloads classes and modules if application files change.
More precisely, if the web server is running and application files have been modified, Rails unloads all autoloaded constants just before the next request is processed. That way, application classes or modules used during that request are going to be autoloaded, thus picking up their current implementation in the file system.
More precisely, if the web server is running and application files have been modified, Rails unloads all autoloaded constants just before the next request is processed. That way, application classes or modules used during that request will be autoloaded again, thus picking up their current implementation in the file system.
Reloading can be enabled or disabled. The setting that controls this behavior is `config.cache_classes`, which is false by default in `development` mode (reloading enabled), and true by default in `production` mode (reloading disabled).
Rails detects files have changed using an evented file monitor (default), or walking the autoload paths, depending on `config.file_watcher`.
Rails uses an evented file monitor to detect files changes by default. It can be configured instead to detect file changes by walking the autoload paths. This is controlled by the `config.file_watcher` setting.
In a Rails console there is no file watcher active regardless of the value of `config.cache_classes`. This is so because, normally, it would be confusing to have code reloaded in the middle of a console session, the same way you generally want an individual request to be served by a consistent, non-changing set of application classes and modules.
In a Rails console there is no file watcher active regardless of the value of `config.cache_classes`. This is because, normally, it would be confusing to have code reloaded in the middle of a console session. Similar to an individual request, you generally want a console session to be served by a consistent, non-changing set of application classes and modules.
However, you can force a reload in the console by executing `reload!`:
@ -142,7 +142,7 @@ irb(main):003:0> User.object_id
=> 70136284426020
```
as you can see, the class object stored in the `User` constant is different after reloading.
As you can see, the class object stored in the `User` constant is different after reloading.
### Reloading and Stale Objects
@ -167,7 +167,7 @@ There are several ways to do this safely. For instance, the application could de
Let's see other situations that involve stale class or module objects.
Check this Rails console session:
Check out this Rails console session:
```irb
irb> joe = User.new
@ -177,7 +177,7 @@ irb> joe.class == alice.class
=> false
```
`joe` is an instance of the original `User` class. When there is a reload, the `User` constant evaluates to a different, reloaded class. `alice` is an instance of the current one, but `joe` is not, his class is stale. You may define `joe` again, start an IRB subsession, or just launch a new console instead of calling `reload!`.
`joe` is an instance of the original `User` class. When there is a reload, the `User` constant then evaluates to a different, reloaded class. `alice` is an instance of the newly loaded `User`, but `joe` is not — his class is stale. You may define `joe` again, start an IRB subsession, or just launch a new console instead of calling `reload!`.
Another situation in which you may find this gotcha is subclassing reloadable classes in a place that is not reloaded:
@ -205,7 +205,7 @@ That block runs when the application boots, and every time code is reloaded.
NOTE: For historical reasons, this callback may run twice. The code it executes must be idempotent.
However, if you do not need to reload the class, it is easier to define it in a directory which does not belong to the autoload paths. For instance, `lib` is an idiomatic choice, it does not belong to the autoload paths by default but it belongs to `$LOAD_PATH`. Then, in the place the class is needed at boot time, just perform a regular `require` to load it.
However, if you do not need to reload the class, it is easier to define it in a directory which does not belong to the autoload paths. For instance, `lib` is an idiomatic choice. It does not belong to the autoload paths by default, but it does belong to `$LOAD_PATH`. Then, in the place the class is needed at boot time, just perform a regular `require` to load it.
For example, there is no point in defining reloadable Rack middleware, because changes would not be reflected in the instance stored in the middleware stack anyway. If `lib/my_app/middleware/foo.rb` defines a middleware class, then in `config/application.rb` you write:
@ -225,19 +225,19 @@ In production-like environments it is generally better to load all the applicati
Eager loading is controlled by the flag `config.eager_load`, which is enabled by default in `production` mode.
The order in which files are eager loaded is undefined.
The order in which files are eager-loaded is undefined.
if the `Zeitwerk` constant is defined, Rails invokes `Zeitwerk::Loader.eager_load_all` regardless of the application autoloading mode. That ensures dependencies managed by Zeitwerk are eager loaded.
If the `Zeitwerk` constant is defined, Rails invokes `Zeitwerk::Loader.eager_load_all` regardless of the application autoloading mode. That ensures dependencies managed by Zeitwerk are eager-loaded.
Single Table Inheritance
------------------------
Single Table Inheritance is a feature that doesn't play well with lazy loading. Reason is, its API generally needs to be able to enumerate the STI hierarchy to work correctly, whereas lazy loading defers loading classes until they are referenced. You can't enumerate what you haven't referenced yet.
Single Table Inheritance is a feature that doesn't play well with lazy loading. The reason is: its API generally needs to be able to enumerate the STI hierarchy to work correctly, whereas lazy loading defers loading classes until they are referenced. You can't enumerate what you haven't referenced yet.
In a sense, applications need to eager load STI hierarchies regardless of the loading mode.
Of course, if the application eager loads on boot, that is already accomplished. When it does not, it is in practice enough to instantiate the existing types in the database, which in development or test modes is usually fine. One way to do that is to throw this module into the `lib` directory:
Of course, if the application eager loads on boot, that is already accomplished. When it does not, it is in practice enough to instantiate the existing types in the database, which in development or test modes is usually fine. One way to do that is to include an STI preloading module in your `lib` directory:
```ruby
module StiPreload
@ -306,7 +306,7 @@ end
Customizing Inflections
-----------------------
By default, Rails uses `String#camelize` to know which constant should a given file or directory name define. For example, `posts_controller.rb` should define `PostsController` because that is what `"posts_controller".camelize` returns.
By default, Rails uses `String#camelize` to know which constant a given file or directory name should define. For example, `posts_controller.rb` should define `PostsController` because that is what `"posts_controller".camelize` returns.
It could be the case that some particular file or directory name does not get inflected as you want. For instance, `html_parser.rb` is expected to define `HtmlParser` by default. What if you prefer the class to be `HTMLParser`? There are a few ways to customize this.
@ -344,22 +344,22 @@ Rails.autoloaders.each do |autoloader|
end
```
There is no global configuration that can affect said instances, they are deterministic.
There is no global configuration that can affect said instances; they are deterministic.
You can even define a custom inflector for full flexibility. Please, check the [Zeitwerk documentation](https://github.com/fxn/zeitwerk#custom-inflector) for further details.
You can even define a custom inflector for full flexibility. Please check the [Zeitwerk documentation](https://github.com/fxn/zeitwerk#custom-inflector) for further details.
Troubleshooting
---------------
The best way to follow what the loaders are doing is to inspect their activity.
The easiest way to do that is to throw
The easiest way to do that is to include
```ruby
Rails.autoloaders.log!
```
to `config/application.rb` after loading the framework defaults. That will print traces to standard output.
in `config/application.rb` after loading the framework defaults. That will print traces to standard output.
If you prefer logging to a file, configure this instead:
@ -367,7 +367,7 @@ If you prefer logging to a file, configure this instead:
Rails.autoloaders.logger = Logger.new("#{Rails.root}/log/autoloading.log")
```
The Rails logger is still not ready in `config/application.rb`, but it is in initializers:
The Rails logger is not yet available when `config/application.rb` executes. If you prefer to use the Rails logger, configure this setting in an initializer instead:
```ruby
# config/initializers/log_autoloaders.rb
@ -406,7 +406,7 @@ class Admin::UsersController < ApplicationController
end
```
was not recommended because the resolution of constants inside their body was brittle. You'd better write them in this style:
was not recommended because the resolution of constants inside their body was brittle. It was better to write them in this style:
```ruby
module Admin
@ -415,11 +415,11 @@ module Admin
end
```
In `zeitwerk` mode that does not matter anymore, you can pick either style.
In `zeitwerk` mode that does not matter anymore. You can pick either style.
The resolution of a constant could depend on load order, the definition of a class or module object could depend on load order, there was edge cases with singleton classes, oftentimes you had to use `require_dependency` as a workaround, .... The guide for `classic` mode documents [these issues](autoloading_and_reloading_constants_classic_mode.html#common-gotchas).
In `classic` mode, the resolution of a constant could depend on load order, the definition of a class or module object could depend on load order, there were edge cases with singleton classes, oftentimes you had to use `require_dependency` as a workaround, .... The list goes on. The guide for `classic` mode documents [these issues](autoloading_and_reloading_constants_classic_mode.html#common-gotchas).
All these problems are solved in `zeitwerk` mode, it just works as expected, and `require_dependency` should not be used anymore, it is no longer needed.
All these problems are solved in `zeitwerk` mode. It just works as expected. `require_dependency` should not be used anymore, because it is no longer needed.
### Less File Lookups
@ -437,26 +437,4 @@ While in common names these operations match, if acronyms or custom inflection r
### More Differences
There are some other subtle differences, please check [this section of _Upgrading Ruby on Rails_](upgrading_ruby_on_rails.html#autoloading) guide for details.
Classic Mode is Deprecated
--------------------------
By now, it is still possible to use `classic` mode. However, `classic` is deprecated and will be eventually removed.
New applications should use `zeitwerk` mode (which is the default), and applications being upgrade are strongly encouraged to migrate to `zeitwerk` mode. Please check the [_Upgrading Ruby on Rails_](upgrading_ruby_on_rails.html#autoloading) guide for details.
Opting Out
----------
Applications can load Rails 6 defaults and still use the classic autoloader this way:
```ruby
# config/application.rb
config.load_defaults 6.0
config.autoloader = :classic
```
That may be handy if upgrading to Rails 6 in different phases, but classic mode is discouraged for new applications.
`zeitwerk` mode is not available in versions of Rails previous to 6.0.
There are some other subtle differences. Please check the [autoloading section](upgrading_ruby_on_rails.html#autoloading) of the _Upgrading Ruby on Rails_] guide for details.

View File

@ -159,13 +159,6 @@ numbers. It also filters out sensitive values of database columns when call `#in
* `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` 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
config.autoloader = :classic
```
### Configuring Assets
* `config.assets.enabled` a flag that controls whether the asset
@ -1044,6 +1037,10 @@ text/javascript image/svg+xml application/postscript application/x-shockwave-fla
The default is `:rails_storage_redirect`.
### Configuring Action Text
* `config.action_text.attachment_tag_name` accepts a string for the HTML tag used to wrap attachments. Defaults to `"action-text-attachment"`.
### Results of `config.load_defaults`
`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.

View File

@ -1430,7 +1430,7 @@ Associations](association_basics.html) guide.
### Adding a Route for Comments
As with the `welcome` controller, we will need to add a route so that Rails
As with the `articles` controller, we will need to add a route so that Rails
knows where we would like to navigate to see `comments`. Open up the
`config/routes.rb` file again, and edit it as follows:

View File

@ -32,7 +32,7 @@
<item id="toc" media-type="application/x-dtbncx+xml" href="toc.ncx" />
<item id="cover" media-type="image/jpg" href="images/rails_guides_kindle_cover.jpg"/>
<item id="cover" media-type="image/jpeg" href="images/rails_guides_kindle_cover.jpg"/>
</manifest>
<spine toc="toc">

View File

@ -64,7 +64,7 @@ Hence, the cookie serves as temporary authentication for the web application. An
* Instead of stealing a cookie unknown to the attacker, they fix a user's session identifier (in the cookie) known to them. Read more about this so-called session fixation later.
The main objective of most attackers is to make money. The underground prices for stolen bank login accounts range from 0.5%-10% of account balance, $0.5-$30 for credit card numbers ($20-$60 with full details), $0.1-$1.5 for identities (Name, SSN, and DOB), $20-$50 for retailer accounts, and $6-$10 for cloud service provider accounts, according to the [Symantec Internet Security Threat Report (2017)](https://www.symantec.com/content/dam/symantec/docs/reports/istr-22-2017-en.pdf).
The main objective of most attackers is to make money. The underground prices for stolen bank login accounts range from 0.5%-10% of account balance, $0.5-$30 for credit card numbers ($20-$60 with full details), $0.1-$1.5 for identities (Name, SSN, and DOB), $20-$50 for retailer accounts, and $6-$10 for cloud service provider accounts, according to the [Symantec Internet Security Threat Report (2017)](https://docs.broadcom.com/docs/istr-22-2017-en).
### Session Storage
@ -657,7 +657,7 @@ SELECT * FROM projects WHERE (name = '') UNION
The result won't be a list of projects (because there is no project with an empty name), but a list of user names and their password. So hopefully you encrypted the passwords in the database! The only problem for the attacker is, that the number of columns has to be the same in both queries. That's why the second query includes a list of ones (1), which will be always the value 1, in order to match the number of columns in the first query.
Also, the second query renames some columns with the AS statement so that the web application displays the values from the user table. Be sure to update your Rails [to at least 2.1.1](http://www.rorsecurity.info/2008/09/08/sql-injection-issue-in-limit-and-offset-parameter/).
Also, the second query renames some columns with the AS statement so that the web application displays the values from the user table. Be sure to update your Rails [to at least 2.1.1](https://rorsecurity.info/journal/2008/09/08/sql-injection-issue-in-limit-and-offset-parameter.html).
#### Countermeasures
@ -669,7 +669,7 @@ Instead of passing a string to the conditions option, you can pass an array to s
Model.where("login = ? AND password = ?", entered_user_name, entered_password).first
```
As you can see, the first part of the array is an SQL fragment with question marks. The sanitized versions of the variables in the second part of the array replace the question marks. Or you can pass a hash for the same result:
As you can see, the first part of the array is a SQL fragment with question marks. The sanitized versions of the variables in the second part of the array replace the question marks. Or you can pass a hash for the same result:
```ruby
Model.where(login: entered_user_name, password: entered_password).first
@ -689,7 +689,7 @@ The most common entry points are message posts, user comments, and guest books,
XSS attacks work like this: An attacker injects some code, the web application saves it and displays it on a page, later presented to a victim. Most XSS examples simply display an alert box, but it is more powerful than that. XSS can steal the cookie, hijack the session, redirect the victim to a fake website, display advertisements for the benefit of the attacker, change elements on the web site to get confidential information or install malicious software through security holes in the web browser.
During the second half of 2007, there were 88 vulnerabilities reported in Mozilla browsers, 22 in Safari, 18 in IE, and 12 in Opera. The [Symantec Global Internet Security threat report](http://eval.symantec.com/mktginfo/enterprise/white_papers/b-whitepaper_internet_security_threat_report_xiii_04-2008.en-us.pdf) also documented 239 browser plug-in vulnerabilities in the last six months of 2007. [Mpack](http://pandalabs.pandasecurity.com/mpack-uncovered/) is a very active and up-to-date attack framework which exploits these vulnerabilities. For criminal hackers, it is very attractive to exploit an SQL-Injection vulnerability in a web application framework and insert malicious code in every textual table column. In April 2008 more than 510,000 sites were hacked like this, among them the British government, United Nations, and many more high profile targets.
During the second half of 2007, there were 88 vulnerabilities reported in Mozilla browsers, 22 in Safari, 18 in IE, and 12 in Opera. The Symantec Global Internet Security threat report also documented 239 browser plug-in vulnerabilities in the last six months of 2007. [Mpack](https://www.pandasecurity.com/en/mediacenter/malware/mpack-uncovered/) is a very active and up-to-date attack framework which exploits these vulnerabilities. For criminal hackers, it is very attractive to exploit a SQL-Injection vulnerability in a web application framework and insert malicious code in every textual table column. In April 2008 more than 510,000 sites were hacked like this, among them the British government, United Nations, and many more high profile targets.
#### HTML/JavaScript Injection
@ -728,7 +728,7 @@ The log files on www.attacker.com will read like this:
GET http://www.attacker.com/_app_session=836c1c25278e5b321d6bea4f19cb57e2
```
You can mitigate these attacks (in the obvious way) by adding the **httpOnly** flag to cookies, so that `document.cookie` may not be read by JavaScript. HTTP only cookies can be used from IE v6.SP1, Firefox v2.0.0.5, Opera 9.5, Safari 4, and Chrome 1.0.154 onwards. But other, older browsers (such as WebTV and IE 5.5 on Mac) can actually cause the page to fail to load. Be warned that cookies [will still be visible using Ajax](https://www.owasp.org/index.php/HTTPOnly#Browsers_Supporting_HttpOnly), though.
You can mitigate these attacks (in the obvious way) by adding the **httpOnly** flag to cookies, so that `document.cookie` may not be read by JavaScript. HTTP only cookies can be used from IE v6.SP1, Firefox v2.0.0.5, Opera 9.5, Safari 4, and Chrome 1.0.154 onwards. But other, older browsers (such as WebTV and IE 5.5 on Mac) can actually cause the page to fail to load. Be warned that cookies [will still be visible using Ajax](https://owasp.org/www-community/HttpOnly#browsers-supporting-httponly), though.
##### Defacement
@ -787,7 +787,7 @@ This example pops up a message box. It will be recognized by the above `sanitize
_In order to understand today's attacks on web applications, it's best to take a look at some real-world attack vectors._
The following is an excerpt from the [Js.Yamanner@m](http://www.symantec.com/security_response/writeup.jsp?docid=2006-061211-4111-99&tabid=1) Yahoo! Mail [worm](http://groovin.net/stuff/yammer.txt). It appeared on June 11, 2006 and was the first webmail interface worm:
The following is an excerpt from the [Js.Yamanner@m Yahoo! Mail worm](https://community.broadcom.com/symantecenterprise/communities/community-home/librarydocuments/viewdocument?DocumentKey=12d8d106-1137-4d7c-8bb4-3ea1faec83fa). It appeared on June 11, 2006 and was the first webmail interface worm:
```html
<img src='http://us.i1.yimg.com/us.yimg.com/i/us/nt/ma/ma_mail_1.gif'
@ -835,7 +835,7 @@ Another problem for the worm's author was the [CSRF security tokens](#cross-site
In the end, he got a 4 KB worm, which he injected into his profile page.
The [moz-binding](https://www.securiteam.com/securitynews/5LP051FHPE.html) CSS property proved to be another way to introduce JavaScript in CSS in Gecko-based browsers (Firefox, for example).
The [moz-binding](https://securiteam.com/securitynews/5LP051FHPE) CSS property proved to be another way to introduce JavaScript in CSS in Gecko-based browsers (Firefox, for example).
#### Countermeasures
@ -845,7 +845,7 @@ This example, again, showed that a restricted list filter is never complete. How
If you want to provide text formatting other than HTML (due to security), use a mark-up language which is converted to HTML on the server-side. [RedCloth](http://redcloth.org/) is such a language for Ruby, but without precautions, it is also vulnerable to XSS.
For example, RedCloth translates `_test_` to `<em>test<em>`, which makes the text italic. However, up to the current version 3.0.4, it is still vulnerable to XSS. Get the [all-new version 4](http://www.redcloth.org) that removed serious bugs. However, even that version has [some security bugs](http://www.rorsecurity.info/journal/2008/10/13/new-redcloth-security.html), so the countermeasures still apply. Here is an example for version 3.0.4:
For example, RedCloth translates `_test_` to `<em>test<em>`, which makes the text italic. However, up to the current version 3.0.4, it is still vulnerable to XSS. Get the [all-new version 4](http://www.redcloth.org) that removed serious bugs. However, even that version has [some security bugs](https://rorsecurity.info/journal/2008/10/13/new-redcloth-security.html), so the countermeasures still apply. Here is an example for version 3.0.4:
```ruby
RedCloth.new('<script>alert(1)</script>').to_html
@ -1027,7 +1027,7 @@ Here is a list of common headers:
* **X-Frame-Options:** _`SAMEORIGIN` in Rails by default_ - allow framing on same domain. Set it to 'DENY' to deny framing at all or remove this header completely if you want to allow framing on all websites.
* **X-XSS-Protection:** _`1; mode=block` in Rails by default_ - use XSS Auditor and block page if XSS attack is detected. Set it to '0;' if you want to switch XSS Auditor off(useful if response contents scripts from request parameters)
* **X-Content-Type-Options:** _`nosniff` in Rails by default_ - stops the browser from guessing the MIME type of a file.
* **X-Content-Security-Policy:** [A powerful mechanism for controlling which sites certain content types can be loaded from](http://w3c.github.io/webappsec/specs/content-security-policy/csp-specification.dev.html)
* **X-Content-Security-Policy:** [A powerful mechanism for controlling which sites certain content types can be loaded from](https://w3c.github.io/webappsec-csp/)
* **Access-Control-Allow-Origin:** Used to control which sites are allowed to bypass same origin policies and send cross-origin requests.
* **Strict-Transport-Security:** [Used to control if the browser is allowed to only access a site over a secure connection](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security)
@ -1192,4 +1192,4 @@ The security landscape shifts and it is important to keep up to date, because mi
* Subscribe to the Rails security [mailing list](https://groups.google.com/forum/#!forum/rubyonrails-security).
* [Brakeman - Rails Security Scanner](https://brakemanscanner.org/) - To perform static security analysis for Rails applications.
* [Mozilla's Web Security Guidelines](https://infosec.mozilla.org/guidelines/web_security.html) - Recommendations on topics covering Content Security Policy, HTTP headers, Cookies, TLS configuration, etc.
* A [good security blog](https://www.owasp.org) including the [Cross-Site scripting Cheat Sheet](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.md).
* A [good security blog](https://owasp.org/) including the [Cross-Site scripting Cheat Sheet](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.md).

View File

@ -2,6 +2,10 @@
*Josh Abernathy*, *Debbie Milburn*
* Add app concern and test keepfiles to generated engine plugins.
*Gannon McGibbon*
* Stop generating a license for in-app plugins.
*Gannon McGibbon*

View File

@ -23,7 +23,7 @@ module Rails
:require_master_key, :credentials, :disable_sandbox, :add_autoload_paths_to_load_path,
:rake_eager_load
attr_reader :encoding, :api_only, :loaded_config_version, :autoloader
attr_reader :encoding, :api_only, :loaded_config_version
def initialize(*)
super
@ -70,7 +70,6 @@ module Rails
@credentials = ActiveSupport::OrderedOptions.new
@credentials.content_path = default_credentials_content_path
@credentials.key_path = default_credentials_key_path
@autoloader = :classic
@disable_sandbox = false
@add_autoload_paths_to_load_path = true
@permissions_policy = nil
@ -129,8 +128,6 @@ module Rails
when "6.0"
load_defaults "5.2"
self.autoloader = :zeitwerk if RUBY_ENGINE == "ruby"
if respond_to?(:action_view)
action_view.default_enforce_utf8 = false
end
@ -156,8 +153,6 @@ module Rails
when "6.1"
load_defaults "6.0"
self.autoloader = :zeitwerk if %w[ruby truffleruby].include?(RUBY_ENGINE)
if respond_to?(:active_record)
active_record.has_many_inversing = true
active_record.legacy_connection_handling = false
@ -366,18 +361,6 @@ module Rails
end
end
def autoloader=(autoloader)
case autoloader
when :classic
@autoloader = autoloader
when :zeitwerk
require "zeitwerk"
@autoloader = autoloader
else
raise ArgumentError, "config.autoloader may be :classic or :zeitwerk, got #{autoloader.inspect} instead"
end
end
def default_log_file
path = paths["log"].first
unless File.exist? File.dirname path

View File

@ -2,6 +2,7 @@
require "active_support/core_ext/string/inflections"
require "active_support/core_ext/array/conversions"
require "zeitwerk"
module Rails
class Application
@ -39,14 +40,10 @@ module Rails
example = autoloaded.first
example_klass = example.constantize.class
if config.autoloader == :zeitwerk
ActiveSupport::DescendantsTracker.clear
ActiveSupport::Dependencies.clear
ActiveSupport::DescendantsTracker.clear
ActiveSupport::Dependencies.clear
unload_message = "#{these} autoloaded #{constants} #{have} been unloaded."
else
unload_message = "`config.autoloader` is set to `#{config.autoloader}`. #{these} autoloaded #{constants} would have been unloaded if `config.autoloader` had been set to `:zeitwerk`."
end
unload_message = "#{these} autoloaded #{constants} #{have} been unloaded."
ActiveSupport::Deprecation.warn(<<~WARNING)
Initialization autoloaded the #{constants} #{enum}.
@ -76,10 +73,8 @@ module Rails
end
initializer :let_zeitwerk_take_over do
if config.autoloader == :zeitwerk
require "active_support/dependencies/zeitwerk_integration"
ActiveSupport::Dependencies::ZeitwerkIntegration.take_over(enable_reloading: !config.cache_classes)
end
require "active_support/dependencies/zeitwerk_integration"
ActiveSupport::Dependencies::ZeitwerkIntegration.take_over(enable_reloading: !config.cache_classes)
end
# Setup default session store if not already set in config/application.rb
@ -113,10 +108,7 @@ module Rails
initializer :eager_load! do
if config.eager_load
ActiveSupport.run_load_hooks(:before_eager_load, self)
# Checks defined?(Zeitwerk) instead of zeitwerk_enabled? because we
# want to eager load any dependency managed by Zeitwerk regardless of
# the autoloading mode of the application.
Zeitwerk::Loader.eager_load_all if defined?(Zeitwerk)
Zeitwerk::Loader.eager_load_all
config.eager_load_namespaces.each(&:eager_load!)
end
end

View File

@ -1,6 +1,7 @@
# frozen_string_literal: true
require "active_support/dependencies/zeitwerk_integration"
require "zeitwerk"
module Rails
module Autoloaders # :nodoc:
@ -41,7 +42,7 @@ module Rails
end
def zeitwerk_enabled?
Rails.configuration.autoloader == :zeitwerk
true
end
end
end

View File

@ -276,8 +276,9 @@ module Rails
GemfileEntry.path("rails", Rails::Generators::RAILS_DEV_PATH)
]
elsif options.edge?
edge_branch = Rails.gem_version.prerelease? ? "main" : [*Rails.gem_version.segments.first(2), "stable"].join("-")
[
GemfileEntry.github("rails", "rails/rails", "main")
GemfileEntry.github("rails", "rails/rails", edge_branch)
]
elsif options.main?
[

View File

@ -1,7 +1,3 @@
<% if namespaced? -%>
require_dependency "<%= namespaced_path %>/application_controller"
<% end -%>
<% module_namespacing do -%>
class <%= class_name %>Controller < ApplicationController
<% actions.each do |action| -%>

View File

@ -26,11 +26,15 @@ module Rails
empty_directory_with_keep_file "app/assets/images/#{namespaced_name}"
end
empty_directory_with_keep_file "app/models/concerns"
empty_directory_with_keep_file "app/controllers/concerns"
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/models/concerns"
empty_directory_with_keep_file "app/controllers/concerns"
empty_directory_with_keep_file "app/mailers" unless options[:skip_action_mailer]
empty_directory_with_keep_file "app/jobs" unless options[:skip_active_job]
@ -90,12 +94,22 @@ module Rails
def test
template "test/test_helper.rb"
template "test/%namespaced_name%_test.rb"
append_file "Rakefile", <<-EOF
#{rakefile_test_tasks}
task default: :test
append_file "Rakefile", <<~EOF
#{rakefile_test_tasks}
task default: :test
EOF
if engine?
empty_directory_with_keep_file "test/fixtures/files"
empty_directory_with_keep_file "test/controllers"
empty_directory_with_keep_file "test/mailers"
empty_directory_with_keep_file "test/models"
empty_directory_with_keep_file "test/integration"
unless api?
empty_directory_with_keep_file "test/helpers"
end
template "test/integration/navigation_test.rb"
end
end

View File

@ -1,7 +1,3 @@
<% if namespaced? -%>
require_dependency "<%= namespaced_path %>/application_controller"
<% end -%>
<% module_namespacing do -%>
class <%= controller_class_name %>Controller < ApplicationController
before_action :set_<%= singular_table_name %>, only: %i[ show update destroy ]

View File

@ -1,7 +1,3 @@
<% if namespaced? -%>
require_dependency "<%= namespaced_path %>/application_controller"
<% end -%>
<% module_namespacing do -%>
class <%= controller_class_name %>Controller < ApplicationController
before_action :set_<%= singular_table_name %>, only: %i[ show edit update destroy ]

View File

@ -1227,7 +1227,6 @@ module ApplicationTests
test "autoloaders" do
app "development"
config = Rails.application.config
assert Rails.autoloaders.zeitwerk_enabled?
assert_instance_of Zeitwerk::Loader, Rails.autoloaders.main
assert_equal "rails.main", Rails.autoloaders.main.tag
@ -1236,24 +1235,6 @@ module ApplicationTests
assert_equal [Rails.autoloaders.main, Rails.autoloaders.once], Rails.autoloaders.to_a
assert_equal ActiveSupport::Dependencies::ZeitwerkIntegration::Inflector, Rails.autoloaders.main.inflector
assert_equal ActiveSupport::Dependencies::ZeitwerkIntegration::Inflector, Rails.autoloaders.once.inflector
config.autoloader = :classic
assert_not Rails.autoloaders.zeitwerk_enabled?
assert_nil Rails.autoloaders.main
assert_nil Rails.autoloaders.once
assert_equal 0, Rails.autoloaders.count
config.autoloader = :zeitwerk
assert Rails.autoloaders.zeitwerk_enabled?
assert_instance_of Zeitwerk::Loader, Rails.autoloaders.main
assert_equal "rails.main", Rails.autoloaders.main.tag
assert_instance_of Zeitwerk::Loader, Rails.autoloaders.once
assert_equal "rails.once", Rails.autoloaders.once.tag
assert_equal [Rails.autoloaders.main, Rails.autoloaders.once], Rails.autoloaders.to_a
assert_equal ActiveSupport::Dependencies::ZeitwerkIntegration::Inflector, Rails.autoloaders.main.inflector
assert_equal ActiveSupport::Dependencies::ZeitwerkIntegration::Inflector, Rails.autoloaders.once.inflector
assert_raises(ArgumentError) { config.autoloader = :unknown }
end
test "config.action_view.cache_template_loading with cache_classes default" do
@ -2768,6 +2749,30 @@ module ApplicationTests
assert_kind_of ActiveSupport::HashWithIndifferentAccess, ActionCable.server.config.cable
end
test "action_text.config.attachment_tag_name is 'action-text-attachment' with Rails 6 defaults" do
add_to_config 'config.load_defaults "6.1"'
app "development"
assert_equal "action-text-attachment", ActionText::Attachment.tag_name
end
test "action_text.config.attachment_tag_name is 'action-text-attachment' without defaults" do
remove_from_config '.*config\.load_defaults.*\n'
app "development"
assert_equal "action-text-attachment", ActionText::Attachment.tag_name
end
test "action_text.config.attachment_tag_name is can be overridden" do
add_to_config "config.action_text.attachment_tag_name = 'link'"
app "development"
assert_equal "link", ActionText::Attachment.tag_name
end
test "ActionMailbox.logger is Rails.logger by default" do
app "development"

View File

@ -26,7 +26,7 @@ class ZeitwerkIntegrationTest < ActiveSupport::TestCase
deps.singleton_class < deps::ZeitwerkIntegration::Decorations
end
test "ActiveSupport::Dependencies is decorated by default" do
test "ActiveSupport::Dependencies is decorated" do
boot
assert decorated?
@ -36,17 +36,6 @@ class ZeitwerkIntegrationTest < ActiveSupport::TestCase
assert_equal [Rails.autoloaders.main, Rails.autoloaders.once], Rails.autoloaders.to_a
end
test "ActiveSupport::Dependencies is not decorated in classic mode" do
add_to_config "config.autoloader = :classic"
boot
assert_not decorated?
assert_not Rails.autoloaders.zeitwerk_enabled?
assert_nil Rails.autoloaders.main
assert_nil Rails.autoloaders.once
assert_equal 0, Rails.autoloaders.count
end
test "autoloaders inflect with Active Support" do
app_file "config/initializers/inflections.rb", <<-RUBY
ActiveSupport::Inflector.inflections(:en) do |inflect|
@ -295,10 +284,12 @@ class ZeitwerkIntegrationTest < ActiveSupport::TestCase
assert $zeitwerk_integration_test_extras
end
test "autoload_paths are set as root dirs of main, and in the same order" do
test "autoload_paths not in autoload_once_paths are set as root dirs of main, and in the same order" do
boot
existing_autoload_paths = deps.autoload_paths.select { |dir| File.directory?(dir) }
existing_autoload_paths = \
deps.autoload_paths.select { |dir| File.directory?(dir) } -
deps.autoload_once_paths
assert_equal existing_autoload_paths, Rails.autoloaders.main.dirs
end
@ -315,7 +306,10 @@ class ZeitwerkIntegrationTest < ActiveSupport::TestCase
extras.each do |extra|
assert_not_includes Rails.autoloaders.main.dirs, extra
end
assert_equal extras, Rails.autoloaders.once.dirs
e1_index = Rails.autoloaders.once.dirs.index(extras.first)
assert e1_index
assert_equal extras, Rails.autoloaders.once.dirs.slice(e1_index, extras.length)
end
test "clear reloads the main autoloader, and does not reload the once one" do
@ -340,42 +334,6 @@ class ZeitwerkIntegrationTest < ActiveSupport::TestCase
assert_equal %i(main_autoloader), $zeitwerk_integration_reload_test
end
test "verbose = true sets the dependencies logger if present" do
boot
logger = Logger.new(File::NULL)
ActiveSupport::Dependencies.logger = logger
ActiveSupport::Dependencies.verbose = true
Rails.autoloaders.each do |autoloader|
assert_same logger, autoloader.logger
end
end
test "verbose = true sets the Rails logger as fallback" do
boot
ActiveSupport::Dependencies.verbose = true
Rails.autoloaders.each do |autoloader|
assert_same Rails.logger, autoloader.logger
end
end
test "verbose = false sets loggers to nil" do
boot
ActiveSupport::Dependencies.verbose = true
Rails.autoloaders.each do |autoloader|
assert autoloader.logger
end
ActiveSupport::Dependencies.verbose = false
Rails.autoloaders.each do |autoloader|
assert_nil autoloader.logger
end
end
test "unhooks" do
boot

View File

@ -863,8 +863,20 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
def test_edge_option
generator([destination_root], edge: true, skip_webpack_install: true)
run_generator_instance
Rails.stub(:gem_version, Gem::Version.new("2.1.0")) do
generator([destination_root], edge: true, skip_webpack_install: true)
run_generator_instance
end
assert_equal 1, @bundle_commands.count("install")
assert_file "Gemfile", %r{^gem\s+["']rails["'],\s+github:\s+["']#{Regexp.escape("rails/rails")}["'],\s+branch:\s+["']2-1-stable["']$}
end
def test_edge_option_during_alpha
Rails.stub(:gem_version, Gem::Version.new("2.1.0.alpha")) do
generator([destination_root], edge: true, skip_webpack_install: true)
run_generator_instance
end
assert_equal 1, @bundle_commands.count("install")
assert_file "Gemfile", %r{^gem\s+["']rails["'],\s+github:\s+["']#{Regexp.escape("rails/rails")}["'],\s+branch:\s+["']main["']$}

Some files were not shown because too many files have changed in this diff Show More