# frozen_string_literal: true 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 [ Array ] fields The fields 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 [ Array ] fields The fields 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 [ Array ] args The fields 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 [ Array ] args The fields 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) if driver == :mongo1x sorting = (options[:sort] || []).dup sorting.push([ field, direction ]) options.store(:sort, sorting) else sorting = (options[:sort] || {}).dup sorting[field] = direction options.store(:sort, sorting) end 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 [ Array ] 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 [ Array ] fields The field names. # @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 ] The names of the forwardable methods. def forwardables public_instance_methods(false) - [ :options, :options= ] end end end end end end