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.
5584 5585 5586 5587 5588 5589 5590 5591 5592 |
# File 'lib/parse/query.rb', line 5584 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.
5804 5805 5806 5807 5808 5809 5810 5811 |
# File 'lib/parse/query.rb', line 5804 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.
5779 5780 5781 |
# File 'lib/parse/query.rb', line 5779 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.
5863 5864 5865 5866 5867 5868 5869 5870 5871 5872 5873 5874 5875 5876 5877 5878 5879 5880 5881 5882 5883 |
# File 'lib/parse/query.rb', line 5863 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.
5830 5831 5832 5833 5834 5835 5836 5837 |
# File 'lib/parse/query.rb', line 5830 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.
5818 5819 5820 5821 5822 5823 5824 5825 |
# File 'lib/parse/query.rb', line 5818 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.
5615 5616 5617 5618 5619 5620 5621 5622 5623 5624 5625 5626 5627 5628 5629 5630 5631 5632 5633 5634 5635 5636 5637 5638 5639 5640 |
# File 'lib/parse/query.rb', line 5615 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.
5667 5668 5669 5670 5671 5672 5673 5674 5675 5676 5677 5678 5679 5680 5681 5682 5683 5684 5685 5686 5687 5688 5689 5690 5691 5692 5693 5694 5695 5696 5697 5698 5699 5700 5701 5702 5703 5704 5705 5706 5707 5708 5709 5710 5711 5712 5713 5714 5715 5716 5717 5718 5719 5720 5721 5722 5723 5724 5725 5726 5727 5728 5729 5730 5731 5732 5733 5734 5735 5736 5737 5738 5739 5740 5741 5742 5743 5744 5745 5746 5747 5748 5749 5750 5751 5752 5753 5754 |
# File 'lib/parse/query.rb', line 5667 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
5760 5761 5762 5763 5764 5765 5766 5767 5768 5769 5770 5771 5772 |
# File 'lib/parse/query.rb', line 5760 def raw(operation, aggregation_expr) formatted_group_field = @query.send(:format_aggregation_field, @group_field) pipeline = build_pipeline(formatted_group_field, aggregation_expr) response = @query.client.aggregate_pipeline( @query.instance_variable_get(:@table), pipeline, headers: {}, **@query.send(:_opts), ) response.result || [] 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.
5657 5658 5659 |
# File 'lib/parse/query.rb', line 5657 def sort(direction = :asc) order(direction) end |
#sum(field) ⇒ Hash
Sum a field for each group.
5789 5790 5791 5792 5793 5794 5795 5796 |
# File 'lib/parse/query.rb', line 5789 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 |