parse-stack-next 5.1.1 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.env.sample +12 -0
  3. data/.env.test +4 -4
  4. data/CHANGELOG.md +545 -0
  5. data/Gemfile +3 -0
  6. data/Gemfile.lock +6 -1
  7. data/README.md +167 -38
  8. data/Rakefile +56 -10
  9. data/docs/atlas_vector_search_guide.md +110 -9
  10. data/docs/mcp_guide.md +433 -0
  11. data/docs/mongodb_direct_guide.md +66 -1
  12. data/docs/mongodb_index_optimization_guide.md +22 -1
  13. data/docs/usage_guide.md +15 -0
  14. data/lib/parse/agent/approval_gate.rb +0 -0
  15. data/lib/parse/agent/constraint_translator.rb +90 -19
  16. data/lib/parse/agent/describe.rb +1 -0
  17. data/lib/parse/agent/errors.rb +16 -0
  18. data/lib/parse/agent/mcp_client.rb +9 -0
  19. data/lib/parse/agent/mcp_dispatcher.rb +139 -7
  20. data/lib/parse/agent/mcp_rack_app.rb +621 -17
  21. data/lib/parse/agent/mcp_subscriptions.rb +607 -0
  22. data/lib/parse/agent/metadata_dsl.rb +58 -0
  23. data/lib/parse/agent/metadata_registry.rb +141 -1
  24. data/lib/parse/agent/prompt_hardening.rb +213 -0
  25. data/lib/parse/agent/result_formatter.rb +18 -3
  26. data/lib/parse/agent/tools.rb +167 -24
  27. data/lib/parse/agent.rb +692 -21
  28. data/lib/parse/client/request.rb +55 -4
  29. data/lib/parse/client/response.rb +4 -0
  30. data/lib/parse/client.rb +205 -7
  31. data/lib/parse/model/classes/installation.rb +27 -10
  32. data/lib/parse/model/classes/user.rb +8 -0
  33. data/lib/parse/model/core/actions.rb +58 -4
  34. data/lib/parse/model/core/embed_managed.rb +19 -14
  35. data/lib/parse/model/core/indexing.rb +108 -16
  36. data/lib/parse/model/core/querying.rb +29 -0
  37. data/lib/parse/model/model.rb +34 -3
  38. data/lib/parse/model/object.rb +1 -0
  39. data/lib/parse/query.rb +90 -24
  40. data/lib/parse/retrieval/agent_tool.rb +369 -0
  41. data/lib/parse/retrieval/chunk.rb +74 -0
  42. data/lib/parse/retrieval/chunker.rb +208 -0
  43. data/lib/parse/retrieval/retriever.rb +274 -0
  44. data/lib/parse/retrieval.rb +10 -0
  45. data/lib/parse/schema.rb +69 -20
  46. data/lib/parse/stack/version.rb +2 -2
  47. data/parse-stack-next.gemspec +1 -1
  48. data/scripts/docker/docker-compose.atlas.yml +14 -10
  49. data/scripts/docker/docker-compose.test.yml +24 -20
  50. data/scripts/docker/mongo-init.js +3 -3
  51. data/scripts/start-parse.sh +10 -0
  52. data/scripts/start_mcp_server.rb +1 -1
  53. data/scripts/test_server_connection.rb +1 -1
  54. data/scripts/vector_prototype/create_vector_index.js +1 -1
  55. data/scripts/vector_prototype/fetch_embeddings.py +2 -2
  56. data/scripts/vector_prototype/query_prototype.rb +1 -1
  57. data/scripts/vector_prototype/run.sh +4 -4
  58. metadata +10 -2
@@ -56,6 +56,15 @@ module Parse
56
56
  _perishable_token _password_history authData _auth_data
57
57
  ].freeze
58
58
 
59
+ # Guards the check-then-append in the index registration helpers. Index
60
+ # declarations happen at class-load time, but an app server that eager-
61
+ # loads models across multiple threads can otherwise have two threads
62
+ # both pass the idempotency check on the same (still-empty) array and
63
+ # append duplicate declarations — producing duplicate `createIndex`
64
+ # calls at migration time. A single shared mutex is sufficient: this is
65
+ # not a hot path, so coarse locking trades nothing for correctness.
66
+ INDEX_REGISTRY_MUTEX = Mutex.new
67
+
59
68
  # Storage for declared indexes. Each entry is a frozen Hash with
60
69
  # the keys `:keys`, `:options`, `:declared_for` (the source-of-truth
61
70
  # symbol list from the `mongo_index` call, for diagnostics).
@@ -89,6 +98,78 @@ module Parse
89
98
  partial: partial, expire_after: expire_after, name: name)
90
99
  end
91
100
 
101
+ # Declare a UNIQUE index on the exact dedup tuple that
102
+ # `first_or_create!` / `create_or_update!` key on. This is the
103
+ # *correctness floor* for the synchronize-create race.
104
+ #
105
+ # The Redis-backed `synchronize:` lock (see {#first_or_create!}) is a
106
+ # latency optimization: in the common path it collapses concurrent
107
+ # callers so only one issues the create. But a lock can be bypassed —
108
+ # a Redis outage, a TTL expiring between the existence check and the
109
+ # write, a caller passing `synchronize: false`, or two app servers
110
+ # whose lock secrets disagree. When that happens, the *database* is the
111
+ # last line of defense. A unique index guarantees, unconditionally, that
112
+ # two racing inserts can't both land: the loser fails with DuplicateValue
113
+ # (Parse error 137), which `first_or_create!` rescues and resolves to the
114
+ # winning row via `_recover_from_duplicate_value`. Lock + index together
115
+ # make the net invariant "exactly one row, every caller sees the same id"
116
+ # hold under any race, not just the happy path.
117
+ #
118
+ # This is thin sugar over `mongo_index(*fields, unique: true, ...)` —
119
+ # it shares the same registration, validation (sensitive-field guard,
120
+ # pointer auto-rewrite, parallel-array / relation / `_id` rejection),
121
+ # and `IndexMigrator` apply path. The name states the intent: these
122
+ # fields form the dedup identity for create-or-update.
123
+ #
124
+ # Defaults match `mongo_index`: **non-sparse**. The index key is kept
125
+ # identical to the query `first_or_create!` re-runs on recovery, so a
126
+ # 137 always corresponds to a row the recovery query (`_scoped_first`
127
+ # on the same `query_attrs`) can find. A sparse or partial index that
128
+ # fires on a condition the recovery query doesn't reproduce would
129
+ # surface a 137 the rescue can't resolve, and the error would re-raise.
130
+ # `sparse:` is meaningful only when a document is missing *every* field
131
+ # in the tuple (a compound sparse index indexes a doc when it has at
132
+ # least one key); since `first_or_create!` always writes the full tuple,
133
+ # it never produces such a row, so sparse does not weaken the floor —
134
+ # leave it off unless out-of-band writers create tuple-less rows you
135
+ # want excluded.
136
+ #
137
+ # @example Single-field dedup floor
138
+ # class Account < Parse::Object
139
+ # property :email, :string
140
+ # unique_index_on :email
141
+ # end
142
+ # Account.apply_indexes! # provisions { email: 1 } unique via the writer
143
+ #
144
+ # @example Compound tuple with a pointer component
145
+ # class Subscription < Parse::Object
146
+ # property :email, :string
147
+ # belongs_to :tenant, as: :user
148
+ # unique_index_on :email, :tenant # key: { email: 1, _p_tenant: 1 } unique
149
+ # end
150
+ #
151
+ # @example Unique within a subset (partial filter escape hatch)
152
+ # # Unique email per tenant, but rows with no tenant may repeat. You
153
+ # # own the filter's lifecycle and must keep first_or_create!'s
154
+ # # recovery query consistent with it.
155
+ # unique_index_on :email, :tenant,
156
+ # partial: { "_p_tenant" => { "$exists" => true } }
157
+ #
158
+ # @param fields [Array<Symbol>] the dedup tuple, in declaration order.
159
+ # Pointer fields auto-rewrite to `_p_<field>` like `mongo_index`.
160
+ # @param sparse [Boolean] default `false`; see the note above on why
161
+ # it does not weaken the floor and when it actually changes behavior.
162
+ # @param partial [Hash, nil] partial-index filter for "unique within a
163
+ # subset". Owner-managed; keep it consistent with the recovery query.
164
+ # @param name [String, nil] explicit index name; defaults to MongoDB
165
+ # auto-naming.
166
+ # @return [Hash] the registered declaration (frozen)
167
+ # @raise [ArgumentError] same guards as `mongo_index`.
168
+ # @see #first_or_create!
169
+ def unique_index_on(*fields, sparse: false, partial: nil, name: nil)
170
+ mongo_index(*fields, unique: true, sparse: sparse, partial: partial, name: name)
171
+ end
172
+
92
173
  # Sugar for a 2dsphere geospatial index. Geopoint columns are
93
174
  # stored in Mongo as GeoJSON `{ type: "Point", coordinates: [lng, lat] }`
94
175
  # which `2dsphere` indexes natively.
@@ -232,13 +313,10 @@ module Parse
232
313
  collection: nil, # nil sentinel means "use the model's parse_class"
233
314
  }.freeze
234
315
 
235
- if mongo_index_declarations.any? { |d| d[:keys] == declaration[:keys] && d[:options] == declaration[:options] && d[:collection] == declaration[:collection] }
236
- # Idempotent redeclaration same class re-opened or sub-class
237
- # inherited; don't accumulate duplicates.
238
- return declaration
239
- end
240
- mongo_index_declarations << declaration
241
- declaration
316
+ # Idempotent redeclaration (same class re-opened or sub-class
317
+ # inherited) is dropped inside the locked append so a duplicate can't
318
+ # slip through under concurrent class loading.
319
+ append_index_declaration(declaration)
242
320
  end
243
321
 
244
322
  # Register one direction of a relation index. The declaration
@@ -252,11 +330,7 @@ module Parse
252
330
  declared_for: [source].freeze,
253
331
  collection: collection,
254
332
  }.freeze
255
- if mongo_index_declarations.any? { |d| d[:keys] == decl[:keys] && d[:collection] == collection }
256
- return decl
257
- end
258
- mongo_index_declarations << decl
259
- decl
333
+ append_index_declaration(decl)
260
334
  end
261
335
 
262
336
  # Register the compound `{owningId: 1, relatedId: 1}` unique index
@@ -274,11 +348,29 @@ module Parse
274
348
  declared_for: [source].freeze,
275
349
  collection: collection,
276
350
  }.freeze
277
- if mongo_index_declarations.any? { |d| d[:keys] == decl[:keys] && d[:options] == decl[:options] && d[:collection] == collection }
278
- return decl
351
+ append_index_declaration(decl)
352
+ end
353
+
354
+ # Append an index declaration, dropping an exact-duplicate redeclaration,
355
+ # under {INDEX_REGISTRY_MUTEX} so concurrent class loading can't race a
356
+ # duplicate past the idempotency check. Two declarations are duplicates
357
+ # when their `:keys`, `:options`, and `:collection` all match (relation
358
+ # declarations carry a frozen `{}` options hash, so this is equivalent to
359
+ # the prior keys+collection check for those paths).
360
+ # @return [Hash] the declaration passed in (whether newly stored or a
361
+ # dropped duplicate), preserving the previous return contract.
362
+ def append_index_declaration(declaration)
363
+ Parse::Core::Indexing::INDEX_REGISTRY_MUTEX.synchronize do
364
+ decls = (@mongo_index_declarations ||= [])
365
+ unless decls.any? { |d|
366
+ d[:keys] == declaration[:keys] &&
367
+ d[:options] == declaration[:options] &&
368
+ d[:collection] == declaration[:collection]
369
+ }
370
+ decls << declaration
371
+ end
279
372
  end
280
- mongo_index_declarations << decl
281
- decl
373
+ declaration
282
374
  end
283
375
 
284
376
  # Translate a property symbol to the wire-format column name a
@@ -368,6 +368,35 @@ module Parse
368
368
  query(constraints).distinct(field)
369
369
  end
370
370
 
371
+ # Groups records by a field and returns a GroupBy (or SortableGroupBy)
372
+ # aggregation object you can call .count, .sum, .average, etc. on.
373
+ # @example
374
+ # Post.group_by(:category).count
375
+ # Post.group_by(:category, sortable: true).count.sort_by_value_desc
376
+ # @param field [Symbol, String] the field to group by.
377
+ # @param opts keyword arguments forwarded to Parse::Query#group_by
378
+ # (flatten_arrays:, sortable:, return_pointers:, mongo_direct:).
379
+ # @return [Parse::GroupBy, Parse::SortableGroupBy]
380
+ # @see Parse::Query#group_by
381
+ def group_by(field, **opts)
382
+ query.group_by(field, **opts)
383
+ end
384
+
385
+ # Groups records by a date field truncated to the given interval and
386
+ # returns a GroupByDate (or SortableGroupByDate) aggregation object.
387
+ # @example
388
+ # Post.group_by_date(:created_at, :month).count
389
+ # Post.group_by_date(:created_at, :day, sortable: true).count.sort_by_value_desc
390
+ # @param field [Symbol, String] the date field to group by.
391
+ # @param interval [Symbol] one of :year, :month, :week, :day, :hour, :minute, :second.
392
+ # @param opts keyword arguments forwarded to Parse::Query#group_by_date
393
+ # (sortable:, return_pointers:, timezone:, mongo_direct:).
394
+ # @return [Parse::GroupByDate, Parse::SortableGroupByDate]
395
+ # @see Parse::Query#group_by_date
396
+ def group_by_date(field, interval, **opts)
397
+ query.group_by_date(field, interval, **opts)
398
+ end
399
+
371
400
  # Find objects matching the constraint ordered by the descending created_at date.
372
401
  # @param constraints (see #all)
373
402
  # @return [Array<Parse::Object>]
@@ -77,6 +77,33 @@ module Parse
77
77
  CLASS_JOB_SCHEDULE = "_JobSchedule"
78
78
  # The internal schema collection in Parse. Managed by Parse Server.
79
79
  CLASS_SCHEMA = "_SCHEMA"
80
+ # Maps the camelized bare name of a Parse Server built-in class to its
81
+ # leading-underscore storage form. Consulted by {String#to_parse_class}
82
+ # ONLY as a fallback when {find_class} cannot resolve the name — which
83
+ # happens at class-declaration time for a built-in whose Ruby class is not
84
+ # yet registered (e.g. +Parse::Installation+ declares +belongs_to :user+
85
+ # before +Parse::User+ is loaded). Without this fallback the conversion
86
+ # froze the wrong literal (+"User"+) into the association +references+ map
87
+ # and pushed it to the server schema as the pointer +targetClass+, which
88
+ # Parse Server then rejected (+Pointer<User>+ vs +Pointer<_User>+). A
89
+ # genuinely-registered class still wins via {find_class}, so a custom
90
+ # +parse_class+ table mapping is never overridden by this map.
91
+ # Keys are the camelized bare names (the form {String#to_parse_class}
92
+ # computes before lookup); only +User+ is actually targeted by a built-in
93
+ # association before its class registers, the rest are hygiene so an app
94
+ # that declares a pointer/relation to any built-in resolves correctly
95
+ # regardless of load order.
96
+ SYSTEM_CLASS_MAP = {
97
+ "User" => CLASS_USER,
98
+ "Role" => CLASS_ROLE,
99
+ "Session" => CLASS_SESSION,
100
+ "Installation" => CLASS_INSTALLATION,
101
+ "Product" => CLASS_PRODUCT,
102
+ "Audience" => CLASS_AUDIENCE,
103
+ "PushStatus" => CLASS_PUSH_STATUS,
104
+ "JobStatus" => CLASS_JOB_STATUS,
105
+ "JobSchedule" => CLASS_JOB_SCHEDULE,
106
+ }.freeze
80
107
  # The type label for hashes containing file data. Used by Parse::File.
81
108
  TYPE_FILE = "File"
82
109
  # The type label for hashes containing geopoints. Used by Parse::GeoPoint.
@@ -286,9 +313,13 @@ class String
286
313
  def to_parse_class(singularize: false)
287
314
  final_class = singularize ? self.singularize.camelize : self.camelize
288
315
  klass = Parse::Model.find_class(final_class) || Parse::Model.find_class(self)
289
- #handles the case that a class has a custom parse table
290
- final_class = klass.parse_class if klass.present?
291
- final_class
316
+ # A registered class wins (handles a custom parse_class table mapping).
317
+ return klass.parse_class if klass.present?
318
+ # Fallback: resolve a built-in system class by name even when its Ruby
319
+ # class is not yet registered (declaration-time load-order). This keeps a
320
+ # `belongs_to :user` declared before Parse::User loads from freezing the
321
+ # literal "User" instead of "_User" into the schema targetClass.
322
+ Parse::Model::SYSTEM_CLASS_MAP[final_class] || final_class
292
323
  end
293
324
  end
294
325
 
@@ -38,6 +38,7 @@ require_relative "core/search_indexing"
38
38
  require_relative "core/properties"
39
39
  require_relative "core/vector_searchable"
40
40
  require_relative "core/embed_managed"
41
+ require_relative "../retrieval"
41
42
  require_relative "core/errors"
42
43
  require_relative "core/builder"
43
44
  require_relative "core/enhanced_change_tracking"
data/lib/parse/query.rb CHANGED
@@ -69,19 +69,46 @@ module Parse
69
69
  include Parse::Client::Connectable
70
70
  include Enumerable
71
71
 
72
- # Known Parse classes for fast validation - dynamically loaded from schema
72
+ # Built-in Parse classes always considered known, independent of the
73
+ # server schema. Used both as the seed for the dynamic list and as the
74
+ # transient fallback when the schema fetch fails.
75
+ BUILT_IN_PARSE_CLASSES = %w[
76
+ _User _Role _Session _Installation _Audience
77
+ User Role Session Installation Audience
78
+ ].freeze
79
+
80
+ # Mutex guarding lazy memoization of {known_parse_classes} so concurrent
81
+ # first-callers don't each fire a `schemas` request and clobber the cache.
82
+ @known_parse_classes_mutex = Mutex.new
83
+
84
+ # Known Parse classes for fast validation - dynamically loaded from schema.
85
+ #
86
+ # The successful result is memoized; a failed schema fetch is NOT cached —
87
+ # it returns the built-in fallback for this call only, so a transient
88
+ # server outage during boot doesn't permanently strip every application-
89
+ # defined class from the known set (which would make class-accessibility
90
+ # checks reject custom classes for the process lifetime). The narrowed
91
+ # rescue logs the failure instead of swallowing it silently.
73
92
  def self.known_parse_classes
74
- @known_parse_classes ||= begin
75
- # Get all classes from Parse schema
93
+ cached = @known_parse_classes
94
+ return cached if cached
95
+
96
+ @known_parse_classes_mutex.synchronize do
97
+ # Re-check under the lock: a racing caller may have populated it.
98
+ return @known_parse_classes if @known_parse_classes
99
+
100
+ begin
76
101
  response = Parse.client.schemas
77
- schema_classes = response.success? ? response.result.dig("results")&.map { |cls| cls["className"] } || [] : []
78
- # Add built-in Parse classes
79
- built_in_classes = %w[_User _Role _Session _Installation _Audience User Role Session Installation Audience]
80
- (built_in_classes + schema_classes).uniq.freeze
81
- rescue
82
- # Fallback to built-in classes if schema query fails (e.g., during testing without server)
83
- %w[_User _Role _Session _Installation _Audience User Role Session Installation Audience].freeze
102
+ schema_classes = response.success? ? response.results.map { |cls| cls["className"] } : []
103
+ @known_parse_classes = (BUILT_IN_PARSE_CLASSES + schema_classes).uniq.freeze
104
+ rescue Parse::Error, Faraday::Error => e
105
+ # Don't cache the fallback — let the next call retry the fetch once
106
+ # the server is reachable again.
107
+ warn "[Parse::Query] schema fetch failed (#{e.class}: #{e.message}); " \
108
+ "falling back to built-in classes for this check only."
109
+ BUILT_IN_PARSE_CLASSES
84
110
  end
111
+ end
85
112
  end
86
113
 
87
114
  # Allow resetting the cached known classes (useful for testing)
@@ -2564,6 +2591,26 @@ module Parse
2564
2591
  def convert_constraints_for_direct_mongodb(constraints)
2565
2592
  return constraints unless constraints.is_a?(Hash)
2566
2593
 
2594
+ # $relatedTo resolves a Parse Relation, which is stored in the
2595
+ # `_Join:<key>:<ParentClass>` collection — a join the SDK does NOT
2596
+ # translate on the mongo-direct path. Passed through verbatim it reaches
2597
+ # MongoDB as an unknown `$match` operator and fails with an opaque error;
2598
+ # and any future attempt to rewrite it into a `$lookup` would have to
2599
+ # re-implement the `_rperm` / protectedFields enforcement that the rest of
2600
+ # this path applies post-fetch. Parse Server's own `$relatedTo` was found
2601
+ # to bypass exactly that enforcement (GHSA-wmwx-jr2p-4j4r), so fail closed
2602
+ # here with a clear message rather than risk a silent leak: this query
2603
+ # must run via REST (the default), where Parse Server resolves the
2604
+ # relation under its own ACL / CLP enforcement.
2605
+ if constraints.key?("$relatedTo") || constraints.key?(:"$relatedTo")
2606
+ raise ArgumentError,
2607
+ "[Parse::Query] $relatedTo cannot run on the mongo-direct path; a " \
2608
+ "Parse Relation is resolved server-side via its join collection. Run " \
2609
+ "this query via REST (omit `mongo_direct:` / `.results_direct` and any " \
2610
+ "direct-only constraint), or express the membership as an `$inQuery` " \
2611
+ "against the relation's join collection."
2612
+ end
2613
+
2567
2614
  result = {}
2568
2615
  constraints.each do |field, value|
2569
2616
  field_str = field.to_s
@@ -6284,8 +6331,10 @@ module Parse
6284
6331
  include Enumerable
6285
6332
 
6286
6333
  # @param results [Hash] the grouped results hash
6287
- def initialize(results)
6334
+ # @param operation [String, nil] the aggregation operation (e.g. "count", "sum", "average", "min", "max", "list")
6335
+ def initialize(results, operation = nil)
6288
6336
  @results = results
6337
+ @operation = operation
6289
6338
  end
6290
6339
 
6291
6340
  # Return the raw hash results
@@ -6332,12 +6381,15 @@ module Parse
6332
6381
 
6333
6382
  # Convert grouped results to a formatted table.
6334
6383
  # @param format [Symbol] output format (:ascii, :csv, :json)
6335
- # @param headers [Array<String>] custom headers (default: ["Group", "Count"])
6384
+ # @param headers [Array<String>, nil] custom headers; if nil, defaults to ["Group", <op-derived header>]
6385
+ # where the second header reflects the aggregation operation (e.g. "Average" for avg/average,
6386
+ # "Sum" for sum, "Min"/"Max" for min/max, "Items" for list, "Count" otherwise).
6336
6387
  # @return [String] formatted table
6337
6388
  # @example
6338
6389
  # Document.group_by(:category, sortable: true).count.to_table
6339
6390
  # Document.group_by(:category).sum(:file_size).to_table(headers: ["Category", "Total Size"])
6340
- def to_table(format: :ascii, headers: ["Group", "Count"])
6391
+ def to_table(format: :ascii, headers: nil)
6392
+ headers ||= ["Group", default_value_header]
6341
6393
  pairs = @results.to_a
6342
6394
 
6343
6395
  # Build table data
@@ -6361,6 +6413,20 @@ module Parse
6361
6413
 
6362
6414
  private
6363
6415
 
6416
+ # Derive a human-readable column header from the aggregation operation.
6417
+ # @return [String] the default second-column header
6418
+ def default_value_header
6419
+ case @operation&.to_s
6420
+ when "count" then "Count"
6421
+ when "sum" then "Sum"
6422
+ when "average", "avg" then "Average"
6423
+ when "min" then "Min"
6424
+ when "max" then "Max"
6425
+ when "list" then "Items"
6426
+ else "Count"
6427
+ end
6428
+ end
6429
+
6364
6430
  # Format group key for display
6365
6431
  def format_group_key(key)
6366
6432
  case key
@@ -6453,7 +6519,7 @@ module Parse
6453
6519
  # @return [GroupedResult] a sortable result object.
6454
6520
  def count
6455
6521
  results = super
6456
- GroupedResult.new(results)
6522
+ GroupedResult.new(results, "count")
6457
6523
  end
6458
6524
 
6459
6525
  # Sum a field for each group.
@@ -6461,7 +6527,7 @@ module Parse
6461
6527
  # @return [GroupedResult] a sortable result object.
6462
6528
  def sum(field)
6463
6529
  results = super
6464
- GroupedResult.new(results)
6530
+ GroupedResult.new(results, "sum")
6465
6531
  end
6466
6532
 
6467
6533
  # Calculate average of a field for each group.
@@ -6469,7 +6535,7 @@ module Parse
6469
6535
  # @return [GroupedResult] a sortable result object.
6470
6536
  def average(field)
6471
6537
  results = super
6472
- GroupedResult.new(results)
6538
+ GroupedResult.new(results, "average")
6473
6539
  end
6474
6540
 
6475
6541
  alias_method :avg, :average
@@ -6479,7 +6545,7 @@ module Parse
6479
6545
  # @return [GroupedResult] a sortable result object.
6480
6546
  def min(field)
6481
6547
  results = super
6482
- GroupedResult.new(results)
6548
+ GroupedResult.new(results, "min")
6483
6549
  end
6484
6550
 
6485
6551
  # Find maximum value of a field for each group.
@@ -6487,14 +6553,14 @@ module Parse
6487
6553
  # @return [GroupedResult] a sortable result object.
6488
6554
  def max(field)
6489
6555
  results = super
6490
- GroupedResult.new(results)
6556
+ GroupedResult.new(results, "max")
6491
6557
  end
6492
6558
 
6493
6559
  # Collect Parse::Object instances per group.
6494
6560
  # @return [GroupedResult] a sortable result object.
6495
6561
  def list
6496
6562
  results = super
6497
- GroupedResult.new(results)
6563
+ GroupedResult.new(results, "list")
6498
6564
  end
6499
6565
  end
6500
6566
 
@@ -7091,7 +7157,7 @@ module Parse
7091
7157
  # @return [GroupedResult] a sortable result object.
7092
7158
  def count
7093
7159
  results = super
7094
- GroupedResult.new(results)
7160
+ GroupedResult.new(results, "count")
7095
7161
  end
7096
7162
 
7097
7163
  # Sum a field for each time period.
@@ -7099,7 +7165,7 @@ module Parse
7099
7165
  # @return [GroupedResult] a sortable result object.
7100
7166
  def sum(field)
7101
7167
  results = super
7102
- GroupedResult.new(results)
7168
+ GroupedResult.new(results, "sum")
7103
7169
  end
7104
7170
 
7105
7171
  # Calculate average of a field for each time period.
@@ -7107,7 +7173,7 @@ module Parse
7107
7173
  # @return [GroupedResult] a sortable result object.
7108
7174
  def average(field)
7109
7175
  results = super
7110
- GroupedResult.new(results)
7176
+ GroupedResult.new(results, "average")
7111
7177
  end
7112
7178
 
7113
7179
  alias_method :avg, :average
@@ -7117,7 +7183,7 @@ module Parse
7117
7183
  # @return [GroupedResult] a sortable result object.
7118
7184
  def min(field)
7119
7185
  results = super
7120
- GroupedResult.new(results)
7186
+ GroupedResult.new(results, "min")
7121
7187
  end
7122
7188
 
7123
7189
  # Find maximum value of a field for each time period.
@@ -7125,7 +7191,7 @@ module Parse
7125
7191
  # @return [GroupedResult] a sortable result object.
7126
7192
  def max(field)
7127
7193
  results = super
7128
- GroupedResult.new(results)
7194
+ GroupedResult.new(results, "max")
7129
7195
  end
7130
7196
  end
7131
7197
  end # Parse