Replace async: true parameter by async_* methods

This commit is contained in:
Jean Boussier 2022-04-04 11:48:34 +02:00
parent 03e435b943
commit e5b08b9ebc
11 changed files with 120 additions and 87 deletions

@ -719,12 +719,12 @@ def destroy(*records)
# # ]
#--
def calculate(operation, column_name, async: false)
null_scope? ? scope.calculate(operation, column_name, async: async) : super
def calculate(operation, column_name)
null_scope? ? scope.calculate(operation, column_name) : super
end
def pluck(*column_names, async: false)
null_scope? ? scope.pluck(*column_names, async: async) : super
def pluck(*column_names)
null_scope? ? scope.pluck(*column_names) : super
end
##

@ -38,14 +38,14 @@ def many?
false
end
def calculate(operation, _column_name, async: false)
def calculate(operation, _column_name)
result = case operation
when :count, :sum
group_values.any? ? Hash.new : 0
when :average, :minimum, :maximum
group_values.any? ? Hash.new : nil
end
async ? Promise::Complete.new(result) : result
@async ? Promise::Complete.new(result) : result
end
def exists?(_conditions = :none)

@ -31,7 +31,7 @@ def value
# Returns a new +ActiveRecord::Promise+ that will apply the passed block
# when the value is accessed:
#
# Post.pluck(:title, async: true).then { |title| title.upcase }.value
# Post.async_pluck(:title).then { |title| title.upcase }.value
# # => "POST TITLE"
def then(&block)
Promise.new(@future_result, @block ? @block >> block : block)

@ -17,7 +17,8 @@ module Querying
:and, :or, :annotate, :optimizer_hints, :extending,
:having, :create_with, :distinct, :references, :none, :unscope, :merge, :except, :only,
:count, :average, :minimum, :maximum, :sum, :calculate,
:pluck, :pick, :ids, :strict_loading, :excluding, :without, :async,
:pluck, :pick, :ids, :strict_loading, :excluding, :without,
:async_count, :async_average, :async_minimum, :async_maximum, :async_sum, :async_pluck, :async_pick,
].freeze # :nodoc:
delegate(*QUERYING_METHODS, to: :all)
@ -46,10 +47,13 @@ module Querying
#
# Note that building your own SQL query string from user input may expose your application to
# injection attacks (https://guides.rubyonrails.org/security.html#sql-injection).
#
# If <tt>async: true</tt> is passed, an {ActiveRecord::Promise} will be returned.
def find_by_sql(sql, binds = [], preparable: nil, async: false, &block)
_query_by_sql(sql, binds, preparable: preparable, async: async).then do |result|
def find_by_sql(sql, binds = [], preparable: nil, &block)
_load_from_sql(_query_by_sql(sql, binds, preparable: preparable), &block)
end
# Same as <tt>#find_by_sql</tt> but perform the query asynchronously and returns an <tt>ActiveRecord::Promise</tt>
def async_find_by_sql(sql, binds = [], preparable: nil, &block)
_query_by_sql(sql, binds, preparable: preparable, async: true).then do |result|
_load_from_sql(result, &block)
end
end
@ -94,10 +98,13 @@ def _load_from_sql(result_set, &block) # :nodoc:
# ==== Parameters
#
# * +sql+ - An SQL statement which should return a count query from the database, see the example above.
#
# If <tt>async: true</tt> is passed, an {ActiveRecord::Promise} will be returned.
def count_by_sql(sql, async: false)
connection.select_value(sanitize_sql(sql), "#{name} Count", async: async).then(&:to_i)
def count_by_sql(sql)
connection.select_value(sanitize_sql(sql), "#{name} Count").to_i
end
# Same as <tt>#count_by_sql</tt> but perform the query asynchronously and returns an <tt>ActiveRecord::Promise</tt>
def async_count_by_sql(sql)
connection.select_value(sanitize_sql(sql), "#{name} Count", async: true).then(&:to_i)
end
end
end

@ -33,6 +33,7 @@ def initialize(klass, table: klass.arel_table, predicate_builder: klass.predicat
@delegate_to_klass = false
@future_result = nil
@records = nil
@async = false
end
def initialize_copy(other)

@ -40,9 +40,7 @@ module Calculations
#
# Note: not all valid {Relation#select}[rdoc-ref:QueryMethods#select] expressions are valid #count expressions. The specifics differ
# between databases. In invalid cases, an error from the database is thrown.
#
# If <tt>async: true</tt> is passed, an {ActiveRecord::Promise} will be returned.
def count(column_name = nil, async: false)
def count(column_name = nil)
if block_given?
unless column_name.nil?
raise ArgumentError, "Column name argument is not supported when a block is passed."
@ -50,18 +48,26 @@ def count(column_name = nil, async: false)
super()
else
calculate(:count, column_name, async: async)
calculate(:count, column_name)
end
end
# Same as <tt>#count</tt> but perform the query asynchronously and returns an <tt>ActiveRecord::Promise</tt>
def async_count(column_name = nil)
async.count(column_name)
end
# Calculates the average value on a given column. Returns +nil+ if there's
# no row. See #calculate for examples with options.
#
# Person.average(:age) # => 35.8
#
# If <tt>async: true</tt> is passed, an {ActiveRecord::Promise} will be returned.
def average(column_name, async: false)
calculate(:average, column_name, async: async)
def average(column_name)
calculate(:average, column_name)
end
# Same as <tt>#average</tt> but perform the query asynchronously and returns an <tt>ActiveRecord::Promise</tt>
def async_average(column_name)
async.average(column_name)
end
# Calculates the minimum value on a given column. The value is returned
@ -69,10 +75,13 @@ def average(column_name, async: false)
# #calculate for examples with options.
#
# Person.minimum(:age) # => 7
#
# If <tt>async: true</tt> is passed, an {ActiveRecord::Promise} will be returned.
def minimum(column_name, async: false)
calculate(:minimum, column_name, async: async)
def minimum(column_name)
calculate(:minimum, column_name)
end
# Same as <tt>#minimum</tt> but perform the query asynchronously and returns an <tt>ActiveRecord::Promise</tt>
def async_minimum(column_name)
async.minimum(column_name)
end
# Calculates the maximum value on a given column. The value is returned
@ -80,10 +89,13 @@ def minimum(column_name, async: false)
# #calculate for examples with options.
#
# Person.maximum(:age) # => 93
#
# If <tt>async: true</tt> is passed, an {ActiveRecord::Promise} will be returned.
def maximum(column_name, async: false)
calculate(:maximum, column_name, async: async)
def maximum(column_name)
calculate(:maximum, column_name)
end
# Same as <tt>#maximum</tt> but perform the query asynchronously and returns an <tt>ActiveRecord::Promise</tt>
def async_maximum(column_name)
async.maximum(column_name)
end
# Calculates the sum of values on a given column. The value is returned
@ -91,9 +103,7 @@ def maximum(column_name, async: false)
# #calculate for examples with options.
#
# Person.sum(:age) # => 4562
#
# If <tt>async: true</tt> is passed, an {ActiveRecord::Promise} will be returned.
def sum(identity_or_column = nil, async: false, &block)
def sum(identity_or_column = nil, &block)
if block_given?
values = map(&block)
if identity_or_column.nil? && (values.first.is_a?(Numeric) || values.first(1) == [])
@ -110,10 +120,15 @@ def sum(identity_or_column = nil, async: false, &block)
values.sum(identity_or_column)
end
else
calculate(:sum, identity_or_column, async: async)
calculate(:sum, identity_or_column)
end
end
# Same as <tt>#sum</tt> but perform the query asynchronously and returns an <tt>ActiveRecord::Promise</tt>
def async_sum(identity_or_column = nil)
async.sum(identity_or_column)
end
# This calculates aggregate values in the given column. Methods for #count, #sum, #average,
# #minimum, and #maximum have been added as shortcuts.
#
@ -145,9 +160,7 @@ def sum(identity_or_column = nil, async: false, &block)
# values.each do |family, max_age|
# ...
# end
#
# If <tt>async: true</tt> is passed, an {ActiveRecord::Promise} will be returned.
def calculate(operation, column_name, async: false)
def calculate(operation, column_name)
if has_include?(column_name)
relation = apply_join_dependency
@ -160,9 +173,9 @@ def calculate(operation, column_name, async: false)
relation.order_values = [] if group_values.empty?
end
relation.calculate(operation, column_name, async: async)
relation.calculate(operation, column_name)
else
perform_calculation(operation, column_name, async: async)
perform_calculation(operation, column_name)
end
end
@ -200,12 +213,10 @@ def calculate(operation, column_name, async: false)
# # => ['0', '27761', '173']
#
# See also #ids.
#
# If <tt>async: true</tt> is passed, an {ActiveRecord::Promise} will be returned.
def pluck(*column_names, async: false)
def pluck(*column_names)
if loaded? && all_attributes?(column_names)
result = records.pluck(*column_names)
if async
if @async
return Promise::Complete.new(result)
else
return result
@ -222,9 +233,9 @@ def pluck(*column_names, async: false)
relation.select_values = columns
result = skip_query_cache_if_necessary do
if where_clause.contradiction?
ActiveRecord::Result.empty(async: async)
ActiveRecord::Result.empty(async: @async)
else
klass.connection.select_all(relation.arel, "#{klass.name} Pluck", async: async)
klass.connection.select_all(relation.arel, "#{klass.name} Pluck", async: @async)
end
end
result.then do |result|
@ -233,6 +244,11 @@ def pluck(*column_names, async: false)
end
end
# Same as <tt>#pluck</tt> but perform the query asynchronously and returns an <tt>ActiveRecord::Promise</tt>
def async_pluck(*column_names)
async.pluck(*column_names)
end
# Pick the value(s) from the named column(s) in the current relation.
# This is short-hand for <tt>relation.limit(1).pluck(*column_names).first</tt>, and is primarily useful
# when you have a relation that's already narrowed down to a single row.
@ -247,15 +263,18 @@ def pluck(*column_names, async: false)
# Person.where(id: 1).pick(:name, :email_address)
# # SELECT people.name, people.email_address FROM people WHERE id = 1 LIMIT 1
# # => [ 'David', 'david@loudthinking.com' ]
#
# If <tt>async: true</tt> is passed, an {ActiveRecord::Promise} will be returned.
def pick(*column_names, async: false)
def pick(*column_names)
if loaded? && all_attributes?(column_names)
result = records.pick(*column_names)
return async ? Promise::Complete.new(result) : result
return @async ? Promise::Complete.new(result) : result
end
limit(1).pluck(*column_names, async: async).then(&:first)
limit(1).pluck(*column_names).then(&:first)
end
# Same as <tt>#pick</tt> but perform the query asynchronously and returns an <tt>ActiveRecord::Promise</tt>
def async_pick(*column_names)
async.pick(*column_names)
end
# Pluck all the ID's for the relation using the table's primary key
@ -275,7 +294,7 @@ def has_include?(column_name)
eager_loading? || (includes_values.present? && column_name && column_name != :all)
end
def perform_calculation(operation, column_name, async: false)
def perform_calculation(operation, column_name)
operation = operation.to_s.downcase
# If #count is used with #distinct (i.e. `relation.distinct.count`) it is
@ -296,9 +315,9 @@ def perform_calculation(operation, column_name, async: false)
end
if group_values.any?
execute_grouped_calculation(operation, column_name, distinct, async: async)
execute_grouped_calculation(operation, column_name, distinct)
else
execute_simple_calculation(operation, column_name, distinct, async: async)
execute_simple_calculation(operation, column_name, distinct)
end
end
@ -318,7 +337,7 @@ def operation_over_aggregate_column(column, operation, distinct)
operation == "count" ? column.count(distinct) : column.public_send(operation)
end
def execute_simple_calculation(operation, column_name, distinct, async: false) # :nodoc:
def execute_simple_calculation(operation, column_name, distinct) # :nodoc:
if operation == "count" && (column_name == :all && distinct || has_limit_or_offset?)
# Shortcut when limit is zero.
return 0 if limit_value == 0
@ -338,7 +357,7 @@ def execute_simple_calculation(operation, column_name, distinct, async: false) #
end
query_result = skip_query_cache_if_necessary do
@klass.connection.select_all(query_builder, "#{@klass.name} #{operation.capitalize}", async: async)
@klass.connection.select_all(query_builder, "#{@klass.name} #{operation.capitalize}", async: @async)
end
query_result.then do |result|
@ -352,7 +371,7 @@ def execute_simple_calculation(operation, column_name, distinct, async: false) #
end
end
def execute_grouped_calculation(operation, column_name, distinct, async: false) # :nodoc:
def execute_grouped_calculation(operation, column_name, distinct) # :nodoc:
group_fields = group_values
group_fields = group_fields.uniq if group_fields.size > 1
@ -390,7 +409,7 @@ def execute_grouped_calculation(operation, column_name, distinct, async: false)
relation.group_values = group_fields
relation.select_values = select_values
result = skip_query_cache_if_necessary { @klass.connection.select_all(relation.arel, "#{@klass.name} #{operation.capitalize}", async: async) }
result = skip_query_cache_if_necessary { @klass.connection.select_all(relation.arel, "#{@klass.name} #{operation.capitalize}", async: @async) }
result.then do |calculated_data|
if association
key_ids = calculated_data.collect { |row| row[group_aliases.first] }

@ -1314,7 +1314,16 @@ def build_where_clause(opts, rest = []) # :nodoc:
end
alias :build_having_clause :build_where_clause
def async!
@async = true
self
end
private
def async
spawn.async!
end
def lookup_table_klass_from_join_dependencies(table_name)
each_join_dependencies do |join|
return join.base_klass if table_name == join.table_name

@ -31,22 +31,22 @@ class CalculationsTest < ActiveRecord::TestCase
def test_should_sum_field
assert_equal 318, Account.sum(:credit_limit)
assert_async_equal 318, Account.sum(:credit_limit, async: true)
assert_async_equal 318, Account.async_sum(:credit_limit)
end
def test_should_sum_arel_attribute
assert_equal 318, Account.sum(Account.arel_table[:credit_limit])
assert_async_equal 318, Account.sum(Account.arel_table[:credit_limit], async: true)
assert_async_equal 318, Account.async_sum(Account.arel_table[:credit_limit])
end
def test_should_average_field
assert_equal 53.0, Account.average(:credit_limit)
assert_async_equal 53.0, Account.average(:credit_limit, async: true)
assert_async_equal 53.0, Account.async_average(:credit_limit)
end
def test_should_average_arel_attribute
assert_equal 53.0, Account.average(Account.arel_table[:credit_limit])
assert_async_equal 53.0, Account.average(Account.arel_table[:credit_limit], async: true)
assert_async_equal 53.0, Account.async_average(Account.arel_table[:credit_limit])
end
def test_should_resolve_aliased_attributes
@ -97,18 +97,18 @@ def test_should_return_nil_as_average
def test_should_get_maximum_of_field
assert_equal 60, Account.maximum(:credit_limit)
assert_async_equal 60, Account.maximum(:credit_limit, async: true)
assert_async_equal 60, Account.async_maximum(:credit_limit)
end
def test_should_get_maximum_of_arel_attribute
assert_equal 60, Account.maximum(Account.arel_table[:credit_limit])
assert_async_equal 60, Account.maximum(Account.arel_table[:credit_limit], async: true)
assert_async_equal 60, Account.async_maximum(Account.arel_table[:credit_limit])
end
def test_should_get_maximum_of_field_with_include
relation = Account.where("companies.name != 'Summit'").references(:companies).includes(:firm)
assert_equal 55, relation.maximum(:credit_limit)
assert_async_equal 55, relation.maximum(:credit_limit, async: true)
assert_async_equal 55, relation.async_maximum(:credit_limit)
end
def test_should_get_maximum_of_arel_attribute_with_include
@ -117,12 +117,12 @@ def test_should_get_maximum_of_arel_attribute_with_include
def test_should_get_minimum_of_field
assert_equal 50, Account.minimum(:credit_limit)
assert_async_equal 50, Account.minimum(:credit_limit, async: true)
assert_async_equal 50, Account.async_minimum(:credit_limit)
end
def test_should_get_minimum_of_arel_attribute
assert_equal 50, Account.minimum(Account.arel_table[:credit_limit])
assert_async_equal 50, Account.minimum(Account.arel_table[:credit_limit], async: true)
assert_async_equal 50, Account.async_minimum(Account.arel_table[:credit_limit])
end
def test_should_group_by_field
@ -130,7 +130,7 @@ def test_should_group_by_field
[1, 6, 2].each do |firm_id|
assert_includes c.keys, firm_id, "Group #{c.inspect} does not contain firm_id #{firm_id}"
end
assert_async_equal c, Account.group(:firm_id).sum(:credit_limit, async: true)
assert_async_equal c, Account.group(:firm_id).async_sum(:credit_limit)
end
def test_should_group_by_arel_attribute
@ -431,7 +431,7 @@ def test_should_group_by_summed_field_with_conditions_and_having
assert_equal 105, c[6]
assert_nil c[2]
assert_async_equal c, relation.sum(:credit_limit, async: true)
assert_async_equal c, relation.async_sum(:credit_limit)
end
def test_should_group_by_fields_with_table_alias
@ -455,9 +455,6 @@ def test_should_calculate_grouped_with_longer_field
def test_should_calculate_with_invalid_field
assert_equal 6, Account.calculate(:count, "*")
assert_equal 6, Account.calculate(:count, :all)
assert_async_equal 6, Account.calculate(:count, "*", async: true)
assert_async_equal 6, Account.calculate(:count, :all, async: true)
end
def test_should_calculate_grouped_with_invalid_field
@ -759,19 +756,19 @@ def test_distinct_is_honored_when_used_with_count_operation_after_group
def test_pluck
assert_equal [1, 2, 3, 4, 5], Topic.order(:id).pluck(:id)
assert_async_equal [1, 2, 3, 4, 5], Topic.order(:id).pluck(:id, async: true)
assert_async_equal [1, 2, 3, 4, 5], Topic.order(:id).async_pluck(:id)
end
def test_pluck_async_on_loaded_relation
relation = Topic.order(:id).load
assert_async_equal relation.pluck(:id), relation.pluck(:id, async: true)
assert_async_equal relation.pluck(:id), relation.async_pluck(:id)
end
def test_pluck_with_empty_in
assert_queries(0) do
assert_equal [], Topic.where(id: []).pluck(:id)
end
assert_async_equal [], Topic.where(id: []).pluck(:id, async: true)
assert_async_equal [], Topic.where(id: []).async_pluck(:id)
end
def test_pluck_without_column_names
@ -1021,7 +1018,7 @@ def test_calculation_with_polymorphic_relation
part.trinkets.create!
assert_equal part.id, ShipPart.joins(:trinkets).sum(:id)
assert_async_equal part.id, ShipPart.joins(:trinkets).sum(:id, async: true)
assert_async_equal part.id, ShipPart.joins(:trinkets).async_sum(:id)
end
def test_pluck_joined_with_polymorphic_relation
@ -1029,7 +1026,7 @@ def test_pluck_joined_with_polymorphic_relation
part.trinkets.create!
assert_equal [part.id], ShipPart.joins(:trinkets).pluck(:id)
assert_async_equal [part.id], ShipPart.joins(:trinkets).pluck(:id, async: true)
assert_async_equal [part.id], ShipPart.joins(:trinkets).async_pluck(:id)
end
def test_pluck_loaded_relation
@ -1071,7 +1068,7 @@ def test_pick_one
assert_nil Topic.where(id: 9999999999999999999).pick(:heading)
end
assert_async_equal "The First Topic", Topic.order(:id).pick(:heading, async: true)
assert_async_equal "The First Topic", Topic.order(:id).async_pick(:heading)
end
def test_pick_two
@ -1081,7 +1078,7 @@ def test_pick_two
assert_nil Topic.where(id: 9999999999999999999).pick(:author_name, :author_email_address)
end
assert_async_equal ["David", "david@loudthinking.com"], Topic.order(:id).pick(:author_name, :author_email_address, async: true)
assert_async_equal ["David", "david@loudthinking.com"], Topic.order(:id).async_pick(:author_name, :author_email_address)
end
def test_pick_delegate_to_all

@ -606,7 +606,7 @@ def test_find_with_entire_select_statement
assert_equal(1, topics.size)
assert_equal(topics(:second).title, topics.first.title)
assert_async_equal topics, Topic.find_by_sql("SELECT * FROM topics WHERE author_name = 'Mary'", async: true)
assert_async_equal topics, Topic.async_find_by_sql("SELECT * FROM topics WHERE author_name = 'Mary'")
end
def test_find_with_prepared_select_statement
@ -1327,7 +1327,7 @@ def test_count_by_sql
assert_equal(1, Entrant.count_by_sql(["SELECT COUNT(*) FROM entrants WHERE id > ?", 2]))
assert_equal(2, Entrant.count_by_sql(["SELECT COUNT(*) FROM entrants WHERE id > ?", 1]))
assert_async_equal 2, Entrant.count_by_sql(["SELECT COUNT(*) FROM entrants WHERE id > ?", 1], async: true)
assert_async_equal 2, Entrant.async_count_by_sql(["SELECT COUNT(*) FROM entrants WHERE id > ?", 1])
end
def test_find_by_one_attribute

@ -78,8 +78,8 @@ def test_null_relation_where_values_hash
define_method "test_null_relation_#{method}_async" do
assert_no_queries do
assert_async_equal 0, Comment.none.public_send(method, :id, async: true)
assert_async_equal Hash.new, Comment.none.group(:post_id).public_send(method, :id, async: true)
assert_async_equal 0, Comment.none.public_send("async_#{method}", :id)
assert_async_equal Hash.new, Comment.none.group(:post_id).public_send("async_#{method}", :id)
end
end
end
@ -94,8 +94,8 @@ def test_null_relation_where_values_hash
define_method "test_null_relation_#{method}_async" do
assert_no_queries do
assert_async_equal nil, Comment.none.public_send(method, :id, async: true)
assert_async_equal Hash.new, Comment.none.group(:post_id).public_send(method, :id, async: true)
assert_async_equal nil, Comment.none.public_send("async_#{method}", :id)
assert_async_equal Hash.new, Comment.none.group(:post_id).public_send("async_#{method}", :id)
end
end
end

@ -52,7 +52,7 @@ class QueryingMethodsDelegationTest < ActiveRecord::TestCase
ActiveRecord::QueryMethods.public_instance_methods(false).reject { |method|
method.end_with?("=", "!", "?", "value", "values", "clause")
} - [:reverse_order, :arel, :extensions, :construct_join_dependency] + [
:async, :any?, :many?, :none?, :one?,
:any?, :many?, :none?, :one?,
:first_or_create, :first_or_create!, :first_or_initialize,
:find_or_create_by, :find_or_create_by!, :find_or_initialize_by,
:create_or_find_by, :create_or_find_by!,