lib/mongoid/criteria/queryable/optional.rb
# frozen_string_literal: true
# rubocop:todo all
module Mongoid
class Criteria
module Queryable
# The optional module includes all behavior that has to do with extra
# options surrounding queries, like skip, limit, sorting, etc.
module Optional
extend Macroable
# @attribute [rw] options The query options.
attr_accessor :options
# Add ascending sorting options for all the provided fields.
#
# @example Add ascending sorting.
# optional.ascending(:first_name, :last_name)
#
# @param [ Symbol... ] *fields The field(s) to sort.
#
# @return [ Optional ] The cloned optional.
def ascending(*fields)
sort_with_list(*fields, 1)
end
alias :asc :ascending
key :asc, :override, 1
key :ascending, :override, 1
# Adds the option for telling MongoDB how many documents to retrieve in
# it's batching.
#
# @example Apply the batch size options.
# optional.batch_size(500)
#
# @param [ Integer ] value The batch size.
#
# @return [ Optional ] The cloned optional.
def batch_size(value = nil)
option(value) { |options| options.store(:batch_size, value) }
end
# Add descending sorting options for all the provided fields.
#
# @example Add descending sorting.
# optional.descending(:first_name, :last_name)
#
# @param [ Symbol... ] *fields The field(s) to sort.
#
# @return [ Optional ] The cloned optional.
def descending(*fields)
sort_with_list(*fields, -1)
end
alias :desc :descending
key :desc, :override, -1
key :descending, :override, -1
# Add an index hint to the query options.
#
# @example Add an index hint.
# optional.hint("$natural" => 1)
#
# @param [ Hash ] value The index hint.
#
# @return [ Optional ] The cloned optional.
def hint(value = nil)
option(value) { |options| options.store(:hint, value) }
end
# Add the number of documents to limit in the returned results.
#
# @example Limit the number of returned documents.
# optional.limit(20)
#
# @param [ Integer ] value The number of documents to return.
#
# @return [ Optional ] The cloned optional.
def limit(value = nil)
option(value) do |options, query|
val = value.to_i
options.store(:limit, val)
query.pipeline.push("$limit" => val) if aggregating?
end
end
# Adds the option to limit the number of documents scanned in the
# collection.
#
# @example Add the max scan limit.
# optional.max_scan(1000)
#
# @param [ Integer ] value The max number of documents to scan.
#
# @return [ Optional ] The cloned optional.
def max_scan(value = nil)
option(value) { |options| options.store(:max_scan, value) }
end
# Adds a cumulative time limit in milliseconds for processing operations on a cursor.
#
# @example Add the max time ms option.
# optional.max_time_ms(200)
#
# @param [ Integer ] value The max time in milliseconds for processing operations on a cursor.
#
# @return [ Optional ] The cloned optional.
def max_time_ms(value = nil)
option(value) { |options| options.store(:max_time_ms, value) }
end
# Tell the query not to timeout.
#
# @example Tell the query not to timeout.
# optional.no_timeout
#
# @return [ Optional ] The cloned optional.
def no_timeout
clone.tap { |query| query.options.store(:timeout, false) }
end
# Limits the results to only contain the fields provided.
#
# @example Limit the results to the provided fields.
# optional.only(:name, :dob)
#
# @param [ Symbol... ] *args The field(s) to return.
#
# @return [ Optional ] The cloned optional.
def only(*args)
args = args.flatten
option(*args) do |options|
options.store(
:fields,
args.inject(options[:fields] || {}){ |sub, field| sub.tap { sub[field] = 1 }},
false
)
end
end
# Adds sorting criterion to the options.
#
# @example Add sorting options via a hash with integer directions.
# optional.order_by(name: 1, dob: -1)
#
# @example Add sorting options via a hash with symbol directions.
# optional.order_by(name: :asc, dob: :desc)
#
# @example Add sorting options via a hash with string directions.
# optional.order_by(name: "asc", dob: "desc")
#
# @example Add sorting options via an array with integer directions.
# optional.order_by([[ name, 1 ], [ dob, -1 ]])
#
# @example Add sorting options via an array with symbol directions.
# optional.order_by([[ :name, :asc ], [ :dob, :desc ]])
#
# @example Add sorting options via an array with string directions.
# optional.order_by([[ "name", "asc" ], [ "dob", "desc" ]])
#
# @example Add sorting options with keys.
# optional.order_by(:name.asc, :dob.desc)
#
# @example Add sorting options via a string.
# optional.order_by("name ASC, dob DESC")
#
# @param [ [ Array | Hash | String ]... ] *spec The sorting specification.
#
# @return [ Optional ] The cloned optional.
def order_by(*spec)
option(spec) do |options, query|
spec.compact.each do |criterion|
criterion.__sort_option__.each_pair do |field, direction|
add_sort_option(options, field, direction)
end
query.pipeline.push("$sort" => options[:sort]) if aggregating?
end
end
end
alias :order :order_by
# Instead of merging the order criteria, use this method to completely
# replace the existing ordering with the provided.
#
# @example Replace the ordering.
# optional.reorder(name: :asc)
#
# @param [ [ Array | Hash | String ]... ] *spec The sorting specification.
#
# @return [ Optional ] The cloned optional.
def reorder(*spec)
clone.tap do |query|
query.options.delete(:sort)
end.order_by(*spec)
end
# Add the number of documents to skip.
#
# @example Add the number to skip.
# optional.skip(100)
#
# @param [ Integer ] value The number to skip.
#
# @return [ Optional ] The cloned optional.
def skip(value = nil)
option(value) do |options, query|
val = value.to_i
options.store(:skip, val)
query.pipeline.push("$skip" => val) if aggregating?
end
end
alias :offset :skip
# Limit the returned results via slicing embedded arrays.
#
# @example Slice the returned results.
# optional.slice(aliases: [ 0, 5 ])
#
# @param [ Hash ] criterion The slice options.
#
# @return [ Optional ] The cloned optional.
def slice(criterion = nil)
option(criterion) do |options|
options.__union__(
fields: criterion.inject({}) do |option, (field, val)|
option.tap { |opt| opt.store(field, { "$slice" => val }) }
end
)
end
end
# Tell the query to operate in snapshot mode.
#
# @example Add the snapshot option.
# optional.snapshot
#
# @return [ Optional ] The cloned optional.
def snapshot
clone.tap do |query|
query.options.store(:snapshot, true)
end
end
# Limits the results to only contain the fields not provided.
#
# @example Limit the results to the fields not provided.
# optional.without(:name, :dob)
#
# @param [ Symbol... ] *args The field(s) to ignore.
#
# @return [ Optional ] The cloned optional.
def without(*args)
args = args.flatten
option(*args) do |options|
options.store(
:fields,
args.inject(options[:fields] || {}){ |sub, field| sub.tap { sub[field] = 0 }},
false
)
end
end
# Associate a comment with the query.
#
# @example Add a comment.
# optional.comment('slow query')
#
# @note Set profilingLevel to 2 and the comment will be logged in the profile
# collection along with the query.
#
# @param [ String ] comment The comment to be associated with the query.
#
# @return [ Optional ] The cloned optional.
def comment(comment = nil)
clone.tap do |query|
query.options.store(:comment, comment)
end
end
# Set the cursor type.
#
# @example Set the cursor type.
# optional.cursor_type(:tailable)
# optional.cursor_type(:tailable_await)
#
# @note The cursor can be type :tailable or :tailable_await.
#
# @param [ Symbol ] type The type of cursor to create.
#
# @return [ Optional ] The cloned optional.
def cursor_type(type)
clone.tap { |query| query.options.store(:cursor_type, type) }
end
# Set the collation.
#
# @example Set the collation.
# optional.collation(locale: 'fr', strength: 2)
#
# @param [ Hash ] collation_doc The document describing the collation to use.
#
# @return [ Optional ] The cloned optional.
def collation(collation_doc)
clone.tap { |query| query.options.store(:collation, collation_doc) }
end
private
# Add a single sort option.
#
# @api private
#
# @example Add a single sort option.
# optional.add_sort_option({}, :name, 1)
#
# @param [ Hash ] options The options.
# @param [ String ] field The field name.
# @param [ Integer ] direction The sort direction.
#
# @return [ Optional ] The cloned optional.
def add_sort_option(options, field, direction)
sorting = (options[:sort] || {}).dup
sorting[field] = direction
options.store(:sort, sorting)
end
# Take the provided criterion and store it as an option in the query
# options.
#
# @api private
#
# @example Store the option.
# optional.option({ skip: 10 })
#
# @param [ Object... ] *args The options.
#
# @return [ Queryable ] The cloned queryable.
def option(*args)
clone.tap do |query|
unless args.compact.empty?
yield(query.options, query)
end
end
end
# Add multiple sort options at once.
#
# @api private
#
# @example Add multiple sort options.
# optional.sort_with_list(:name, :dob, 1)
#
# @param [ [ Symbol | String ]... ] *fields The field name(s).
# @param [ Integer ] direction The sort direction.
#
# @return [ Optional ] The cloned optional.
def sort_with_list(*fields, direction)
option(fields) do |options, query|
fields.flatten.compact.each do |field|
add_sort_option(options, field, direction)
end
query.pipeline.push("$sort" => options[:sort]) if aggregating?
end
end
class << self
# Get the methods on the optional that can be forwarded to from a model.
#
# @example Get the forwardable methods.
# Optional.forwardables
#
# @return [ Array<Symbol> ] The names of the forwardable methods.
def forwardables
public_instance_methods(false) - [ :options, :options= ]
end
end
end
end
end
end