module MongoDoc
module Contexts
class Mongo
include Mongoid::Contexts::Paging
include Mongoid::Contexts::Ids
attr_reader :criteria, :cache
delegate :klass, :options, :selector, :to => :criteria
delegate :collection, :to => :klass
AGGREGATE_REDUCE = "function(obj, prev) { prev.count++; }"
# Aggregate the context. This will take the internally built selector and options
# and pass them on to the Ruby driver's +group()+ method on the collection. The
# collection itself will be retrieved from the class provided, and once the
# query has returned it will provided a grouping of keys with counts.
#
# Example:
#
# context.aggregate
#
# Returns:
#
# A +Hash+ with field values as keys, counts as values
def aggregate
collection.group(options[:fields], selector, { :count => 0 }, AGGREGATE_REDUCE, true)
end
# Get the average value for the supplied field.
#
# This will take the internally built selector and options
# and pass them on to the Ruby driver's +group()+ method on the collection. The
# collection itself will be retrieved from the class provided, and once the
# query has returned it will provided a grouping of keys with averages.
#
# Example:
#
# context.avg(:age)
#
# Returns:
#
# A numeric value that is the average.
def avg(field)
total = sum(field)
total ? (total / count) : nil
end
# Get the count of matching documents in the database for the context.
#
# Example:
#
# context.count
#
# Returns:
#
# An +Integer+ count of documents.
def count
@count ||= collection.find(selector, options).count
end
# Gets an array of distinct values for the supplied field across the
# entire collection or the susbset given the criteria.
#
# Example:
#
# context.distinct(:title)
def distinct(field)
collection.distinct(field, selector)
end
# Determine if the context is empty or blank given the criteria. Will
# perform a quick find_one asking only for the id.
#
# Example:
#
# context.blank?
def empty?
collection.find_one(selector, options).nil?
end
alias blank? empty?
# Execute the context. This will take the selector and options
# and pass them on to the Ruby driver's +find()+ method on the collection. The
# collection itself will be retrieved from the class provided, and once the
# query has returned new documents of the type of class provided will be instantiated.
#
# Example:
#
# mongo.execute
#
# Returns:
#
# An enumerable +Cursor+.
def execute(paginating = false)
cursor = collection.find(selector, options)
if cursor
@count = cursor.count if paginating
cursor
else
[]
end
end
GROUP_REDUCE = "function(obj, prev) { prev.group.push(obj); }"
# Groups the context. This will take the internally built selector and options
# and pass them on to the Ruby driver's +group()+ method on the collection. The
# collection itself will be retrieved from the class provided, and once the
# query has returned it will provided a grouping of keys with objects.
#
# Example:
#
# context.group
#
# Returns:
#
# A +Hash+ with field values as keys, arrays of documents as values.
def group
collection.group(
options[:fields],
selector,
{ :group => [] },
GROUP_REDUCE,
true
).collect {|docs| docs["group"] = MongoDoc::BSON.decode(docs["group"]); docs }
end
# Create the new mongo context. This will execute the queries given the
# selector and options against the database.
#
# Example:
#
# Mongoid::Contexts::Mongo.new(criteria)
def initialize(criteria)
@criteria = criteria
end
# Iterate over each +Document+ in the results. This can take an optional
# block to pass to each argument in the results.
#
# Example:
#
# context.iterate { |doc| p doc }
def iterate(&block)
return caching(&block) if criteria.cached?
if block_given?
execute.each do |doc|
yield doc
end
end
end
# Return the last result for the +Context+. Essentially does a find_one on
# the collection with the sorting reversed. If no sorting parameters have
# been provided it will default to ids.
#
# Example:
#
# context.last
#
# Returns:
#
# The last document in the collection.
def last
sorting = options[:sort] || [[:_id, :asc]]
options[:sort] = sorting.collect { |option| [ option[0], option[1].invert ] }
collection.find_one(selector, options)
end
MAX_REDUCE = "function(obj, prev) { if (prev.max == 'start') { prev.max = obj.[field]; } " +
"if (prev.max < obj.[field]) { prev.max = obj.[field]; } }"
# Return the max value for a field.
#
# This will take the internally built selector and options
# and pass them on to the Ruby driver's +group()+ method on the collection. The
# collection itself will be retrieved from the class provided, and once the
# query has returned it will provided a grouping of keys with sums.
#
# Example:
#
# context.max(:age)
#
# Returns:
#
# A numeric max value.
def max(field)
grouped(:max, field.to_s, MAX_REDUCE)
end
MIN_REDUCE = "function(obj, prev) { if (prev.min == 'start') { prev.min = obj.[field]; } " +
"if (prev.min > obj.[field]) { prev.min = obj.[field]; } }"
# Return the min value for a field.
#
# This will take the internally built selector and options
# and pass them on to the Ruby driver's +group()+ method on the collection. The
# collection itself will be retrieved from the class provided, and once the
# query has returned it will provided a grouping of keys with sums.
#
# Example:
#
# context.min(:age)
#
# Returns:
#
# A numeric minimum value.
def min(field)
grouped(:min, field.to_s, MIN_REDUCE)
end
# Return the first result for the +Context+.
#
# Example:
#
# context.one
#
# Return:
#
# The first document in the collection.
def one
collection.find_one(selector, options)
end
alias :first :one
SUM_REDUCE = "function(obj, prev) { if (prev.sum == 'start') { prev.sum = 0; } prev.sum += obj.[field]; }"
# Sum the context.
#
# This will take the internally built selector and options
# and pass them on to the Ruby driver's +group()+ method on the collection. The
# collection itself will be retrieved from the class provided, and once the
# query has returned it will provided a grouping of keys with sums.
#
# Example:
#
# context.sum(:age)
#
# Returns:
#
# A numeric value that is the sum.
def sum(field)
grouped(:sum, field.to_s, SUM_REDUCE)
end
# Common functionality for grouping operations. Currently used by min, max
# and sum. Will gsub the field name in the supplied reduce function.
def grouped(start, field, reduce)
result = collection.group(
nil,
selector,
{ start => "start" },
reduce.gsub("[field]", field),
true
)
result.empty? ? nil : result.first[start.to_s]
end
protected
# Iterate and cache results from execute
def caching(&block)
if cache
cache.each(&block)
else
@cache = []
execute.each do |doc|
@cache << doc
yield doc if block_given?
end
end
end
end
end
end