Class: Parse::GroupBy
- Inherits:
-
Object
- Object
- Parse::GroupBy
- Defined in:
- lib/parse/query.rb
Overview
Helper class for handling group_by aggregations with method chaining. Supports count, sum, average, min, max operations on grouped data. Can optionally flatten array fields before grouping to count individual array elements.
Direct Known Subclasses
Instance Method Summary collapse
-
#average(field) ⇒ Hash
(also: #avg)
Calculate average of a field for each group.
-
#count ⇒ Hash
Count the number of items in each group.
-
#initialize(query, group_field, flatten_arrays: false, return_pointers: false, mongo_direct: false) ⇒ GroupBy
constructor
A new instance of GroupBy.
-
#list ⇒ Hash{Object => Array<Parse::Object>}
Collect every document of each group into an array of Parse::Object instances.
-
#max(field) ⇒ Hash
Find maximum value of a field for each group.
-
#min(field) ⇒ Hash
Find minimum value of a field for each group.
-
#order(spec) ⇒ self
Order grouped results by the group key, the aggregated value, or (for #list) the size of the per-group array.
-
#pipeline ⇒ Array<Hash>
Returns the MongoDB aggregation pipeline that would be used for a count operation.
-
#raw(operation, aggregation_expr) ⇒ Array<Hash>
Returns raw unprocessed aggregation results.
-
#sort(direction = :asc) ⇒ self
Sort grouped results by the group key.
-
#sum(field) ⇒ Hash
Sum a field for each group.
Constructor Details
#initialize(query, group_field, flatten_arrays: false, return_pointers: false, mongo_direct: false) ⇒ GroupBy
Returns a new instance of GroupBy.
6475 6476 6477 6478 6479 6480 6481 6482 6483 |
# File 'lib/parse/query.rb', line 6475 def initialize(query, group_field, flatten_arrays: false, return_pointers: false, mongo_direct: false) @query = query @group_field = group_field @flatten_arrays = flatten_arrays @return_pointers = return_pointers @mongo_direct = mongo_direct @sort_target = nil # nil | :key | :value | :size @sort_direction = nil # :asc | :desc end |
Instance Method Details
#average(field) ⇒ Hash Also known as: avg
Calculate average of a field for each group.
6702 6703 6704 6705 6706 6707 6708 6709 |
# File 'lib/parse/query.rb', line 6702 def average(field) if field.nil? || !field.respond_to?(:to_s) raise ArgumentError, "Invalid field name passed to `average`." end formatted_field = @query.send(:format_aggregation_field, field) execute_group_aggregation("average", { "$avg" => "$#{formatted_field}" }) end |
#count ⇒ Hash
Count the number of items in each group.
6677 6678 6679 |
# File 'lib/parse/query.rb', line 6677 def count execute_group_aggregation("count", { "$sum" => 1 }) end |
#list ⇒ Hash{Object => Array<Parse::Object>}
On the Parse REST /aggregate path there is no ACL/CLP/protectedFields
enforcement — that endpoint is master-key-only. On the mongo-direct path
the SDK's ACL $match runs before $group, and both ACL redaction and
protectedFields stripping recurse into pushed arrays, so scoped agents
get correctly filtered records. The Array recursion that makes this
safe lives in Parse::ACLScope#redact_subdocs! (lib/parse/acl_scope.rb)
and Parse::CLPScope#walk_and_delete! (lib/parse/clp_scope.rb); if you
change either of those, re-verify .list still strips correctly.
Collect every document of each group into an array of Parse::Object
instances. Implemented as $push: "$$ROOT", so each group's value is
the full set of underlying records (subject to the query's where
constraints).
Use this when you want the actual records per group, not just an
aggregated scalar. Combine with .order(size: :desc) to surface the
largest groups first.
6761 6762 6763 6764 6765 6766 6767 6768 6769 6770 6771 6772 6773 6774 6775 6776 6777 6778 6779 6780 6781 |
# File 'lib/parse/query.rb', line 6761 def list table = @query.instance_variable_get(:@table) # `$push: "$$ROOT"` pushes the raw MongoDB-storage-format document # into the result array on BOTH the REST and mongo-direct paths — # Parse Server's aggregate envelope only rewrites the outermost row's # `_id` to `objectId`, not nested arrays. So `_id`, `_p_<field>` # pointer strings, `_acl`/`_rperm`/`_wperm`, and `_created_at`/ # `_updated_at` all survive into the pushed docs and have to be # normalized to Parse shape before `Parse::Object.build` will produce # an instance with the right id, associations, ACL, and timestamps. require_relative "mongodb" build_object = lambda do |doc| parse_doc = Parse::MongoDB.convert_document_to_parse(doc, table) parse_doc ? Parse::Object.build(parse_doc, table) : nil end execute_group_aggregation("list", { "$push" => "$$ROOT" }) do |docs| next [] unless docs.is_a?(Array) docs.map(&build_object).compact end end |
#max(field) ⇒ Hash
Find maximum value of a field for each group.
6728 6729 6730 6731 6732 6733 6734 6735 |
# File 'lib/parse/query.rb', line 6728 def max(field) if field.nil? || !field.respond_to?(:to_s) raise ArgumentError, "Invalid field name passed to `max`." end formatted_field = @query.send(:format_aggregation_field, field) execute_group_aggregation("max", { "$max" => "$#{formatted_field}" }) end |
#min(field) ⇒ Hash
Find minimum value of a field for each group.
6716 6717 6718 6719 6720 6721 6722 6723 |
# File 'lib/parse/query.rb', line 6716 def min(field) if field.nil? || !field.respond_to?(:to_s) raise ArgumentError, "Invalid field name passed to `min`." end formatted_field = @query.send(:format_aggregation_field, field) execute_group_aggregation("min", { "$min" => "$#{formatted_field}" }) end |
#order(spec) ⇒ self
Order grouped results by the group key, the aggregated value, or
(for #list) the size of the per-group array. The ordering is pushed
down into the aggregation pipeline as a $sort stage (plus a
$addFields helper for :size), so MongoDB does the sort and the
returned Hash preserves the order via Ruby's insertion semantics.
6506 6507 6508 6509 6510 6511 6512 6513 6514 6515 6516 6517 6518 6519 6520 6521 6522 6523 6524 6525 6526 6527 6528 6529 6530 |
# File 'lib/parse/query.rb', line 6506 def order(spec) target, direction = case spec when Symbol [:key, spec] when Hash unless spec.size == 1 raise ArgumentError, "order(...) expects a single pair, e.g. {value: :desc} (got #{spec.inspect})" end k, v = spec.first [k.to_sym, v.to_sym] else raise ArgumentError, "order(...) expects {key:|value:|size: => :asc|:desc} or :asc/:desc (got #{spec.inspect})" end unless %i[key value size].include?(target) raise ArgumentError, "order(...) target must be :key, :value, or :size (got #{target.inspect})" end unless %i[asc desc].include?(direction) raise ArgumentError, "order(...) direction must be :asc or :desc (got #{direction.inspect})" end @sort_target = target @sort_direction = direction self end |
#pipeline ⇒ Array<Hash>
Returns the MongoDB aggregation pipeline that would be used for a count operation. This is useful for debugging and understanding the generated pipeline.
6557 6558 6559 6560 6561 6562 6563 6564 6565 6566 6567 6568 6569 6570 6571 6572 6573 6574 6575 6576 6577 6578 6579 6580 6581 6582 6583 6584 6585 6586 6587 6588 6589 6590 6591 6592 6593 6594 6595 6596 6597 6598 6599 6600 6601 6602 6603 6604 6605 6606 6607 6608 6609 6610 6611 6612 6613 6614 6615 6616 6617 6618 6619 6620 6621 6622 6623 6624 6625 6626 6627 6628 6629 6630 6631 6632 6633 6634 6635 6636 6637 6638 6639 6640 6641 6642 6643 6644 |
# File 'lib/parse/query.rb', line 6557 def pipeline # This introspection builds the same shape as the count execution # path (`$sum: 1`), so reject order/aggregation combinations that # the count path would reject at runtime — otherwise the preview # silently produces a pipeline the SDK would never actually run. validate_sort_target_for_operation!("count") # Format the group field name formatted_group_field = @query.send(:format_aggregation_field, @group_field) # Build the aggregation pipeline (same logic as execute_group_aggregation) pipeline = [] # Add match stage if there are where conditions. `compile_where` # is already marker-free; use `compile_markers` to extract # __aggregation_pipeline stages. compiled_where = @query.send(:compile_where) markers = @query.send(:compile_markers) if compiled_where.present? || markers.key?("__aggregation_pipeline") # Collect all match conditions to merge into a single $match stage match_conditions = [] non_match_stages = [] # `compiled_where` is marker-free already. regular_constraints = compiled_where if regular_constraints.present? aggregation_where = @query.send(:convert_constraints_for_aggregation, regular_constraints) stringified_where = @query.send(:convert_dates_for_aggregation, aggregation_where) match_conditions << stringified_where end # Extract aggregation pipeline stages and merge $match stages if markers.key?("__aggregation_pipeline") markers["__aggregation_pipeline"].each do |stage| if stage.is_a?(Hash) && stage.key?("$match") # Extract the $match condition for merging match_conditions << stage["$match"] else # Non-$match stages go directly to pipeline non_match_stages << stage end end end # Combine all match conditions into a single $match stage if match_conditions.any? if match_conditions.length == 1 pipeline << { "$match" => match_conditions.first } else # Use $and to combine multiple match conditions pipeline << { "$match" => { "$and" => match_conditions } } end end # Add any non-$match stages from the aggregation pipeline pipeline.concat(non_match_stages) end # Add unwind stage if flatten_arrays is enabled if @flatten_arrays pipeline << { "$unwind" => "$#{formatted_group_field}" } end # Add group stage (using count as example aggregation) pipeline << { "$group" => { "_id" => "$#{formatted_group_field}", "count" => { "$sum" => 1 }, }, } # Add $addFields + $sort stages if ordering was configured. Sort happens # before $project so we can reference `_id` (pre-rename) for :key sorts. add_fields = size_addfields_stage pipeline << add_fields if add_fields sort = sort_stage pipeline << sort if sort pipeline << { "$project" => { "_id" => 0, "objectId" => "$_id", "count" => 1, }, } pipeline end |
#raw(operation, aggregation_expr) ⇒ Array<Hash>
Returns raw unprocessed aggregation results
6650 6651 6652 6653 6654 6655 6656 6657 6658 6659 6660 6661 6662 6663 6664 6665 6666 6667 6668 6669 6670 |
# File 'lib/parse/query.rb', line 6650 def raw(operation, aggregation_expr) formatted_group_field = @query.send(:format_aggregation_field, @group_field) # Build the same pipeline the count/sum/etc. terminals use, then delegate # to Query#aggregate. That central path handles scoped-query routing # (session_token / acl_user / acl_role / ambient Parse.with_session → # auto-promote to mongo-direct, or fail closed when unavailable) so a # scoped `raw` is never sent to the master-key-only REST /aggregate # endpoint, and it returns the raw Array<Hash> rows this method documents. # `$match` from the query's where constraints is added by Query#aggregate. pipeline = [] pipeline << { "$unwind" => "$#{formatted_group_field}" } if @flatten_arrays pipeline << { "$group" => { "_id" => "$#{formatted_group_field}", "count" => aggregation_expr } } add_fields = size_addfields_stage pipeline << add_fields if add_fields sort = sort_stage pipeline << sort if sort pipeline << { "$project" => { "_id" => 0, "objectId" => "$_id", "count" => 1 } } @query.aggregate(pipeline, verbose: @query.instance_variable_get(:@verbose_aggregate)).raw || [] end |
#sort(direction = :asc) ⇒ self
Sort grouped results by the group key. Alias for order(key: direction),
mirroring Ruby's Hash#sort default. For value-based ordering use
#order explicitly (e.g. .order(value: :desc)).
Note the asymmetry with chaining: .sort.count pushes the sort into
the aggregation pipeline and returns a Hash keyed by group, while
.count.sort first materializes the Hash and then calls Hash#sort,
which returns an Array<[key, value]>. Both order by key ascending
by default; this method exists so the pipeline form is also available.
6547 6548 6549 |
# File 'lib/parse/query.rb', line 6547 def sort(direction = :asc) order(direction) end |
#sum(field) ⇒ Hash
Sum a field for each group.
6687 6688 6689 6690 6691 6692 6693 6694 |
# File 'lib/parse/query.rb', line 6687 def sum(field) if field.nil? || !field.respond_to?(:to_s) raise ArgumentError, "Invalid field name passed to `sum`." end formatted_field = @query.send(:format_aggregation_field, field) execute_group_aggregation("sum", { "$sum" => "$#{formatted_field}" }) end |