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.
5687 5688 5689 5690 5691 5692 5693 5694 5695 |
# File 'lib/parse/query.rb', line 5687 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.
5907 5908 5909 5910 5911 5912 5913 5914 |
# File 'lib/parse/query.rb', line 5907 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.
5882 5883 5884 |
# File 'lib/parse/query.rb', line 5882 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.
5966 5967 5968 5969 5970 5971 5972 5973 5974 5975 5976 5977 5978 5979 5980 5981 5982 5983 5984 5985 5986 |
# File 'lib/parse/query.rb', line 5966 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.
5933 5934 5935 5936 5937 5938 5939 5940 |
# File 'lib/parse/query.rb', line 5933 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.
5921 5922 5923 5924 5925 5926 5927 5928 |
# File 'lib/parse/query.rb', line 5921 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.
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 |
# File 'lib/parse/query.rb', line 5718 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.
5770 5771 5772 5773 5774 5775 5776 5777 5778 5779 5780 5781 5782 5783 5784 5785 5786 5787 5788 5789 5790 5791 5792 5793 5794 5795 5796 5797 5798 5799 5800 5801 5802 5803 5804 5805 5806 5807 5808 5809 5810 5811 5812 5813 5814 5815 5816 5817 5818 5819 5820 5821 5822 5823 5824 5825 5826 5827 5828 5829 5830 5831 5832 5833 5834 5835 5836 5837 5838 5839 5840 5841 5842 5843 5844 5845 5846 5847 5848 5849 5850 5851 5852 5853 5854 5855 5856 5857 |
# File 'lib/parse/query.rb', line 5770 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
5863 5864 5865 5866 5867 5868 5869 5870 5871 5872 5873 5874 5875 |
# File 'lib/parse/query.rb', line 5863 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.
5760 5761 5762 |
# File 'lib/parse/query.rb', line 5760 def sort(direction = :asc) order(direction) end |
#sum(field) ⇒ Hash
Sum a field for each group.
5892 5893 5894 5895 5896 5897 5898 5899 |
# File 'lib/parse/query.rb', line 5892 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 |