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.
6310 6311 6312 6313 6314 6315 6316 6317 6318 |
# File 'lib/parse/query.rb', line 6310 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.
6538 6539 6540 6541 6542 6543 6544 6545 |
# File 'lib/parse/query.rb', line 6538 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.
6513 6514 6515 |
# File 'lib/parse/query.rb', line 6513 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.
6597 6598 6599 6600 6601 6602 6603 6604 6605 6606 6607 6608 6609 6610 6611 6612 6613 6614 6615 6616 6617 |
# File 'lib/parse/query.rb', line 6597 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.
6564 6565 6566 6567 6568 6569 6570 6571 |
# File 'lib/parse/query.rb', line 6564 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.
6552 6553 6554 6555 6556 6557 6558 6559 |
# File 'lib/parse/query.rb', line 6552 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.
6341 6342 6343 6344 6345 6346 6347 6348 6349 6350 6351 6352 6353 6354 6355 6356 6357 6358 6359 6360 6361 6362 6363 6364 6365 6366 |
# File 'lib/parse/query.rb', line 6341 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.
6393 6394 6395 6396 6397 6398 6399 6400 6401 6402 6403 6404 6405 6406 6407 6408 6409 6410 6411 6412 6413 6414 6415 6416 6417 6418 6419 6420 6421 6422 6423 6424 6425 6426 6427 6428 6429 6430 6431 6432 6433 6434 6435 6436 6437 6438 6439 6440 6441 6442 6443 6444 6445 6446 6447 6448 6449 6450 6451 6452 6453 6454 6455 6456 6457 6458 6459 6460 6461 6462 6463 6464 6465 6466 6467 6468 6469 6470 6471 6472 6473 6474 6475 6476 6477 6478 6479 6480 |
# File 'lib/parse/query.rb', line 6393 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
6486 6487 6488 6489 6490 6491 6492 6493 6494 6495 6496 6497 6498 6499 6500 6501 6502 6503 6504 6505 6506 |
# File 'lib/parse/query.rb', line 6486 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.
6383 6384 6385 |
# File 'lib/parse/query.rb', line 6383 def sort(direction = :asc) order(direction) end |
#sum(field) ⇒ Hash
Sum a field for each group.
6523 6524 6525 6526 6527 6528 6529 6530 |
# File 'lib/parse/query.rb', line 6523 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 |