Class: Parse::Query

Inherits:
Object
  • Object
show all
Extended by:
ActiveModel::Callbacks
Includes:
Enumerable, Client::Connectable
Defined in:
lib/parse/query.rb,
lib/parse/model/core/actions.rb

Overview

The Query class provides the lower-level querying interface for your Parse collections by utilizing the REST Querying interface. This is the main engine behind making Parse queries on remote collections. It takes a set of constraints and generates the proper hash parameters that are passed to an API request in order to retrive matching results. The querying design pattern is inspired from DataMapper where symbols are overloaded with specific methods with attached values.

At the core of each item is a Operation. An operation is made up of a field name and an operator. Therefore calling something like :name.eq, defines an equality operator on the field name. Using Operations with values, we can build different types of constraints, known as Constraints.

This component can be used on its own without defining your models as all results are provided in hash form.

Field-Formatter

By convention in Ruby (see Style Guide), symbols and variables are expressed in lower_snake_case form. Parse, however, prefers column names in String#columnize format (ex. objectId, createdAt and updatedAt). To keep in line with the style guides between the languages, we do the automatic conversion of the field names when compiling the query. This feature can be overridden by changing the value of Query.field_formatter.

# default uses :columnize query = Parse::User.query :field_one => 1, :FieldTwo => 2, :Field_Three => 3 query.compile_where # => "fieldTwo"=>2, "fieldThree"=>3

# turn off Parse::Query.field_formatter = nil query = Parse::User.query :field_one => 1, :FieldTwo => 2, :Field_Three => 3 query.compile_where # => "FieldTwo"=>2, "Field_Three"=>3

# force everything camel case Parse::Query.field_formatter = :camelize query = Parse::User.query :field_one => 1, :FieldTwo => 2, :Field_Three => 3 query.compile_where # => "FieldTwo"=>2, "FieldThree"=>3

Most of the constraints supported by Parse are available to Parse::Query. Assuming you have a column named field, here are some examples. For an explanation of the constraints, please see Parse Query Constraints documentation. You can build your own custom query constraints by creating a Parse::Constraint subclass. For all these where clauses assume q is a Parse::Query object.

Defined Under Namespace

Classes: MongoDirectRequired, PointerShapeError

Constant Summary collapse

BUILT_IN_PARSE_CLASSES =

Built-in Parse classes always considered known, independent of the server schema. Used both as the seed for the dynamic list and as the transient fallback when the schema fetch fails.

%w[
  _User _Role _Session _Installation _Audience
  User Role Session Installation Audience
].freeze
QUERY_OPTION_KEYS =

The set of symbol keys that #conditions treats as query-shape options (cache TTL, ordering, limits, ACL convenience helpers, session/master-key overrides) rather than as field-name constraints. External callers that need to partition a user-supplied constraints Hash into "real constraints vs query options" — most notably Parse::Object.first_or_create! and Parse::Object.create_or_update!, which must hand a Hash containing ONLY constraint key/value pairs to Parse::CreateLock.canonicalize_attrs — consult this set via option_key?.

Keep this list in sync with the option branches at the top of #conditions. Anything conditions() extracts as a query parameter rather than a constraint belongs here.

[
  :order, :keys, :key, :skip, :limit,
  :include, :includes,
  :cache, :use_master_key, :session,
  :read_preference,
  :readable_by, :writable_by, :readable_by_role, :writable_by_role,
  :publicly_readable, :publicly_writable,
  :privately_readable, :master_key_read_only,
  :privately_writable, :master_key_write_only,
  :private_acl, :master_key_only,
  :not_publicly_readable, :not_publicly_writable,
].to_set.freeze
HINT_UNSET =

Set a MongoDB index hint for this query. Forces Parse Server (and the underlying MongoDB driver) to use the named index instead of the query planner's choice. Useful for benchmarking or for working around sub-optimal plan selection. The hint is emitted in the compiled REST query body as the +hint+ parameter (supported by Parse Server 7.4.0+) AND forwarded to the mongo-direct path — +results_direct+ / +count_direct+ / +distinct_direct+ pass it to MongoDB.aggregate/MongoDB.find as the Mongo +hint+ option, so a plan diagnosed with #explain can be corrected on either path.

Examples:

Force a specific index

Post.query(:status => "published").hint("status_1_created_at_-1").results

Returns:

:_hint_unset_
RESERVED_EXCLUDE_KEYS =

Reserved fields that #redact_excluded_keys! never strips: dropping these would break #decode (objectId / className / __type) or remove the required Parse envelope. Both the Parse-format names (objectId, createdAt, updatedAt, ACL) and their Mongo storage-form counterparts (_id, _created_at, _updated_at, _acl) are guarded, so the redaction is safe even if it is ever pointed at a raw Mongo document, and a caller can't break reconstruction by excluding e.g. :_id. This is an SDK safety choice, not an assertion about which fields Parse Server's REST excludeKeys strips.

%w[
  objectId className __type createdAt updatedAt ACL
  _id _created_at _updated_at _acl
].freeze
BLOCKED_PIPELINE_STAGES =
Deprecated.

Retained for backwards compatibility. The canonical list now lives in PipelineSecurity::DENIED_OPERATORS and is enforced recursively, not only at the top-level stage.

Create an Aggregation object for executing arbitrary MongoDB pipelines Pipeline stages that are blocked to prevent data exfiltration or destructive operations.

Examples:

pipeline = [
  { "$match" => { "status" => "active" } },
  { "$group" => { "_id" => "$category", "count" => { "$sum" => 1 } } }
]
aggregation = Document.query.aggregate(pipeline)
results = aggregation.results
raw_results = aggregation.raw
pointer_results = aggregation.result_pointers

# With verbose output
aggregation = Document.query.aggregate(pipeline, verbose: true)
# With MongoDB direct (required for $inQuery constraints in aggregation)
aggregation = Document.query.aggregate(pipeline, mongo_direct: true)

Returns:

  • (Aggregation)

    an aggregation object that can be executed

Parse::PipelineSecurity::DENIED_OPERATORS

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#new(table) ⇒ Query #new(parseSubclass) ⇒ Query

Constructor method to create a query with constraints for a specific Parse collection. Also sets the default limit count to :max.

Overloads:

  • #new(table) ⇒ Query

    Create a query for this Parse collection name.

    Examples:

    Parse::Query.new "_User"
    Parse::Query.new "_Installation", :device_type => 'ios'

    Parameters:

    • table (String)

      the name of the Parse collection to query. (ex. "_User")

    • constraints (Hash)

      a set of query constraints.

  • #new(parseSubclass) ⇒ Query

    Create a query for this Parse model (or anything that responds to Object.parse_class).

    Examples:

    Parse::Query.new Parse::User
    # assume Post < Parse::Object
    Parse::Query.new Post, like_count.gt => 0

    Parameters:

    • parseSubclass (Parse::Object)

      the Parse model constant

    • constraints (Hash)

      a set of query constraints.

Raises:

  • (ArgumentError)


488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
# File 'lib/parse/query.rb', line 488

def initialize(table, constraints = {})
  table = table.to_s.to_parse_class if table.is_a?(Symbol)
  table = table.parse_class if table.respond_to?(:parse_class)
  raise ArgumentError, "First parameter should be the name of the Parse class (table)" unless table.is_a?(String)
  @count = 0 #non-zero/1 implies a count query request
  @where = []
  @order = []
  @keys = []
  @exclude_keys = []
  @includes = []
  @limit = nil
  @skip = 0
  @table = table
  @cache = Parse.default_query_cache
  # Tri-state: `nil` means "no caller preference" — the request layer
  # then applies the master-key default, the `Parse.client_mode` flag,
  # and the `Parse.with_session` ambient as configured. Explicit
  # `true` / `false` (set via `use_master_key=` or the `use_master_key:`
  # constraint key) wins over both. A `true` default here would
  # silently smuggle the master-key header past every client-mode
  # query, so we deliberately leave the decision to the request layer
  # unless the caller said otherwise.
  @use_master_key = nil
  @verbose_aggregate = false
  @hint = nil
  conditions constraints
end

Class Attribute Details

.allow_scope_introspectionSymbol

The method to use when converting field names to Parse column names. Default is String#columnize. By convention Parse uses lowercase-first camelcase syntax for field/column names, but ruby uses snakecase. To support this methodology we process all field constraints through the method defined by the field formatter. You may set this to nil to turn off this functionality.

Returns:



# File 'lib/parse/query.rb', line 281

.field_formatterSymbol

The method to use when converting field names to Parse column names. Default is String#columnize. By convention Parse uses lowercase-first camelcase syntax for field/column names, but ruby uses snakecase. To support this methodology we process all field constraints through the method defined by the field formatter. You may set this to nil to turn off this functionality.

Returns:



295
296
297
# File 'lib/parse/query.rb', line 295

def field_formatter
  @field_formatter
end

Instance Attribute Details

#acl_roleParse::Role, ... (readonly)

Returns the role the query was scoped to via #scope_to_role, or nil.

Returns:



1933
1934
1935
# File 'lib/parse/query.rb', line 1933

def acl_role
  @acl_role
end

#acl_userParse::User, ... (readonly)

Returns the user the query was scoped to via #scope_to_user, or nil for unscoped queries.

Returns:



1929
1930
1931
# File 'lib/parse/query.rb', line 1929

def acl_user
  @acl_user
end

#cacheBoolean, Integer

Set whether this query should be cached and for how long. This parameter is used to cache queries when using Middleware::Caching. If the caching middleware is configured, all queries will be cached for the duration allowed by the cache, and therefore some queries could return cached results. To disable caching and cached results for this specific query, you may set this field to false. To specify the specific amount of time you want this query to be cached, set a duration (in number of seconds) that the caching middleware should cache this request.

Examples:

# find all users with name "Bob"
query = Parse::Query.new("_User", :name => "Bob")

query.cache = true # (default) cache using default cache duration.

query.cache = 1.day # cache for 86400 seconds

query.cache = false # do not cache or use cache results

# You may optionally pass this into the constraint hash.
query = Parse::Query.new("_User", :name => "Bob", :cache => 1.day)

Returns:

  • (Boolean)

    if set to true or false on whether it should use the default caching length set when configuring Middleware::Caching.

  • (Integer)

    if set to a number of seconds to cache this specific request with the Middleware::Caching.



210
# File 'lib/parse/query.rb', line 210

attr_reader :table, :session_token

#clientParse::Client

Returns the client to use for making the API request.

Returns:

  • (Parse::Client)

    the client to use for making the API request.

See Also:



210
# File 'lib/parse/query.rb', line 210

attr_reader :table, :session_token

#keyString

This parameter is used to support select queries where you have to pass a key parameter for matching different tables.

Returns:

  • (String)

    the foreign key to match against.



210
# File 'lib/parse/query.rb', line 210

attr_reader :table, :session_token

#read_preferenceSymbol, String

Set the MongoDB read preference for this query. This allows directing read queries to secondary replicas for load balancing.

Examples:

query = Parse::Query.new("_User")
query.read_preference = :secondary  # read from secondary replicas
# Valid values: :primary, :primary_preferred, :secondary, :secondary_preferred, :nearest

Returns:



210
# File 'lib/parse/query.rb', line 210

attr_reader :table, :session_token

#session_tokenObject

Returns the value of attribute session_token.



210
# File 'lib/parse/query.rb', line 210

attr_reader :table, :session_token

#tableString

Returns the name of the Parse collection to query against.

Returns:

  • (String)

    the name of the Parse collection to query against.



210
211
212
# File 'lib/parse/query.rb', line 210

def table
  @table
end

#use_master_keyBoolean

True or false on whether we should send the master key in this request. If You have provided the master_key when initializing Parse, then all requests will send the master key by default. This feature is useful when you want to make a particular query be performed with public credentials, or on behalf of a user using a #session_token. Default is set to true.

Examples:

# disable use of the master_key in the request.
query = Parse::Query.new("_User", :name => "Bob", :master_key => false)

Returns:

  • (Boolean)

    whether we should send the master key in this request.

See Also:



210
# File 'lib/parse/query.rb', line 210

attr_reader :table, :session_token

#verbose_aggregateObject

Returns the value of attribute verbose_aggregate.



212
213
214
# File 'lib/parse/query.rb', line 212

def verbose_aggregate
  @verbose_aggregate
end

Class Method Details

.all(table, constraints = { limit: :max }) ⇒ Query

Helper method to create a query with constraints for a specific Parse collection. Also sets the default limit count to :max.

Parameters:

  • table (String)

    the name of the Parse collection to query. (ex. "_User")

  • constraints (Hash) (defaults to: { limit: :max })

    a set of query constraints.

Returns:

  • (Query)

    a new query for the Parse collection with the passed in constraints.



358
359
360
# File 'lib/parse/query.rb', line 358

def all(table, constraints = { limit: :max })
  self.new(table, constraints.reverse_merge({ limit: :max }))
end

.and(*queries) ⇒ Parse::Query

Combines multiple queries with AND logic using full pipeline approach Each query's complete constraint set is ANDed together

Parameters:

Returns:

Raises:

  • (ArgumentError)

    if the queries don't all target the same Parse class



5511
5512
5513
5514
5515
5516
5517
5518
5519
5520
5521
5522
5523
5524
5525
5526
5527
5528
5529
5530
5531
5532
5533
5534
5535
5536
5537
5538
5539
5540
5541
5542
# File 'lib/parse/query.rb', line 5511

def self.and(*queries)
  queries = queries.flatten.compact
  return nil if queries.empty?

  # Get the table from the first query
  table = queries.first.table

  # Ensure all queries are for the same table
  unless queries.all? { |q| q.table == table }
    raise ArgumentError, "All queries passed to Parse::Query.and must be for the same Parse class."
  end

  # Start with an empty query for this table
  result = self.new(table)

  # Filter to only queries that have constraints
  queries = queries.filter { |q| q.where.present? && !q.where.empty? }

  # Add each query's complete constraint set with AND logic
  # Multiple constraints in a query are implicitly ANDed together by Parse
  queries.each do |query|
    # Compile the where constraints to check if they result in empty conditions
    compiled_where = Parse::Query.compile_where(query.where)
    unless compiled_where.empty?
      # Directly append constraints to result's where array
      # (where method only accepts Hash, but query.where returns Array<Constraint>)
      result.instance_variable_get(:@where).concat(query.where)
    end
  end

  result
end

.compile_markers(where) ⇒ Hash

Return the un-stripped reduced hash so the routing/pipeline layer can inspect __-prefixed markers (e.g. "__mongo_direct_only", "__aggregation_pipeline"). These markers are SDK-internal hints and must never be sent to Parse REST or MongoDB — that's what compile_where is for.

Parameters:

Returns:

  • (Hash)

    the reduced hash including internal markers.



397
398
399
# File 'lib/parse/query.rb', line 397

def compile_markers(where)
  constraint_reduce(where)
end

.compile_where(where) ⇒ Object



386
387
388
# File 'lib/parse/query.rb', line 386

def compile_where(where)
  constraint_reduce(where).reject { |k, _| k.is_a?(String) && k.start_with?("__") }
end

.format_field(str) ⇒ String

Returns formatted string using field_formatter.

Parameters:

  • str (String)

    the string to format

Returns:



305
306
307
308
309
310
311
# File 'lib/parse/query.rb', line 305

def format_field(str)
  res = str.to_s.strip
  if field_formatter.present? && res.respond_to?(field_formatter)
    res = res.send(field_formatter)
  end
  res
end

.known_parse_classesObject

Known Parse classes for fast validation - dynamically loaded from schema.

The successful result is memoized; a failed schema fetch is NOT cached — it returns the built-in fallback for this call only, so a transient server outage during boot doesn't permanently strip every application- defined class from the known set (which would make class-accessibility checks reject custom classes for the process lifetime). The narrowed rescue logs the failure instead of swallowing it silently.



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/parse/query.rb', line 92

def self.known_parse_classes
  cached = @known_parse_classes
  return cached if cached

  @known_parse_classes_mutex.synchronize do
    # Re-check under the lock: a racing caller may have populated it.
    return @known_parse_classes if @known_parse_classes

    begin
      response = Parse.client.schemas
      schema_classes = response.success? ? response.results.map { |cls| cls["className"] } : []
      @known_parse_classes = (BUILT_IN_PARSE_CLASSES + schema_classes).uniq.freeze
    rescue Parse::Error, Faraday::Error => e
      # Don't cache the fallback — let the next call retry the fetch once
      # the server is reachable again.
      warn "[Parse::Query] schema fetch failed (#{e.class}: #{e.message}); " \
           "falling back to built-in classes for this check only."
      BUILT_IN_PARSE_CLASSES
    end
  end
end

.option_key?(key) ⇒ Boolean

Note:

QUERY_OPTION_KEYS must be kept in sync with the option-branch keys recognized at the top of #conditions. When adding a new query option, update BOTH places — this predicate is the public-facing source of truth for callers that partition query_attrs into constraints vs options (notably Object.first_or_create! and Object.create_or_update! for lock canonicalization), and the option-branch in conditions is what actually absorbs the option onto the query.

Whether key is one of the QUERY_OPTION_KEYS that #conditions absorbs as a query-shape option rather than a field-name constraint. Accepts Symbol or String; returns false for any other type (including Parse::Operation, which is always a constraint).

Parameters:

Returns:

  • (Boolean)


273
274
275
276
# File 'lib/parse/query.rb', line 273

def option_key?(key)
  return false unless key.is_a?(Symbol) || key.is_a?(String)
  QUERY_OPTION_KEYS.include?(key.to_sym)
end

.or(*queries) ⇒ Parse::Query

Combines multiple queries with OR logic using full pipeline approach Each query's complete constraint set becomes one branch of the OR condition

Parameters:

Returns:

Raises:

  • (ArgumentError)

    if the queries don't all target the same Parse class



5476
5477
5478
5479
5480
5481
5482
5483
5484
5485
5486
5487
5488
5489
5490
5491
5492
5493
5494
5495
5496
5497
5498
5499
5500
5501
5502
5503
5504
# File 'lib/parse/query.rb', line 5476

def self.or(*queries)
  queries = queries.flatten.compact
  return nil if queries.empty?

  # Get the table from the first query
  table = queries.first.table

  # Ensure all queries are for the same table
  unless queries.all? { |q| q.table == table }
    raise ArgumentError, "All queries passed to Parse::Query.or must be for the same Parse class."
  end

  # Start with an empty query for this table
  result = self.new(table)

  # Filter to only queries that have constraints
  queries = queries.filter { |q| q.where.present? && !q.where.empty? }

  # Add each query's complete constraint set as an OR branch
  queries.each do |query|
    # Compile the where constraints to check if they result in empty conditions
    compiled_where = Parse::Query.compile_where(query.where)
    unless compiled_where.empty?
      result.or_where(query.where)
    end
  end

  result
end

.parse_keys_to_nested_keys(keys) ⇒ Hash

Parses keys patterns to build a map of nested fetched keys. Handles arbitrary nesting depth (e.g., "a.b.c.d" creates entries for a, b, c). For example, ["project.name", "project.status", "author.email"] becomes: { project: [:name, :status], author: [:email] }

Parameters:

  • keys (Array<Symbol, String>)

    the keys patterns (may include dot notation for nested fields)

Returns:

  • (Hash)

    a map of nested field names to their fetched keys



326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
# File 'lib/parse/query.rb', line 326

def parse_keys_to_nested_keys(keys)
  return {} if keys.nil? || keys.empty?

  nested_map = {}

  keys.each do |key_path|
    parts = key_path.to_s.split(".")
    # Skip keys without dots - they're top-level fields, not nested
    next if parts.length < 2

    # Process each level of nesting
    # For path "a.b.c.d": a gets b, b gets c, c gets d
    parts.each_with_index do |part, index|
      field_name = part.to_sym
      nested_map[field_name] ||= []

      # If there's a next part, add it to this field's nested keys
      if index < parts.length - 1
        next_field = parts[index + 1].to_sym
        nested_map[field_name] << next_field unless nested_map[field_name].include?(next_field)
      end
    end
  end

  nested_map
end

.pointer_shape_warnedObject

Process-wide [table, field] cache for warn-once dedup in #handle_unresolvable_pointer_in_array!.



299
300
301
# File 'lib/parse/query.rb', line 299

def pointer_shape_warned
  @pointer_shape_warned ||= {}
end

.reset_known_parse_classes!Object

Allow resetting the cached known classes (useful for testing)



115
116
117
# File 'lib/parse/query.rb', line 115

def self.reset_known_parse_classes!
  @known_parse_classes = nil
end

.to_snake_case(str) ⇒ String

Convert camelCase string to snake_case

Parameters:

  • str (String)

    the camelCase string

Returns:

  • (String)

    the snake_case string



316
317
318
# File 'lib/parse/query.rb', line 316

def to_snake_case(str)
  str.to_s.underscore
end

Instance Method Details

#add_constraint(operator, value = nil, opts = {}) ⇒ self

Add a constraint to the query. This is mainly used internally for compiling constraints.

Examples:

# add where :field equals "value"
query.add_constraint(:field.eq, "value")

# add where :like_count is greater than 20
query.add_constraint(:like_count.gt, 20)

# same, but ignore field formatting
query.add_constraint(:like_count.gt, 20, filter: false)

Parameters:

  • operator (Parse::Operator)

    an operator object containing the operation and operand.

  • value (Object) (defaults to: nil)

    the value for the constraint.

  • opts (Object) (defaults to: {})

    A set of options. Passing :filter with false, will skip field formatting.

Returns:

  • (self)

See Also:



940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
# File 'lib/parse/query.rb', line 940

def add_constraint(operator, value = nil, opts = {})
  @where ||= []
  constraint = operator # assume Parse::Constraint
  unless constraint.is_a?(Parse::Constraint)
    constraint = Parse::Constraint.create(operator, value)
  end
  return unless constraint.is_a?(Parse::Constraint)
  # to support select queries where you have to pass a `key` parameter for matching
  # different tables.
  if constraint.operand == :key || constraint.operand == "key"
    @key = constraint.value
    return
  end

  unless opts[:filter] == false
    constraint.operand = Query.format_field(constraint.operand)
  end
  reject_vector_constraint!(constraint)
  @where.push constraint
  @results = nil
  self #chaining
end

#add_constraints(list) ⇒ self

Combine a list of Constraint objects

Parameters:

Returns:

  • (self)


918
919
920
921
922
# File 'lib/parse/query.rb', line 918

def add_constraints(list)
  list = Array.wrap(list).select { |m| m.is_a?(Parse::Constraint) }
  @where = @where + list
  self
end

#after_prepare { ... } ⇒ Object

A callback called after the query is compiled

Yields:

  • A block to execute for the callback.

See Also:

  • ActiveModel::Callbacks


129
# File 'lib/parse/query.rb', line 129

define_model_callbacks :prepare, only: [:after, :before]

#aggregate(pipeline, verbose: nil, mongo_direct: nil, rewrite_lookups: nil, raw_values: false, raw_field_names: false) ⇒ Object Also known as: aggregate_pipeline



3379
3380
3381
3382
3383
3384
3385
3386
3387
3388
3389
3390
3391
3392
3393
3394
3395
3396
3397
3398
3399
3400
3401
3402
3403
3404
3405
3406
3407
3408
3409
3410
3411
3412
3413
3414
3415
3416
3417
3418
3419
3420
3421
3422
3423
3424
3425
3426
3427
3428
3429
3430
3431
3432
3433
3434
3435
3436
3437
3438
3439
3440
3441
3442
3443
3444
3445
3446
3447
3448
3449
3450
3451
3452
3453
3454
3455
3456
3457
3458
3459
3460
3461
3462
3463
3464
3465
3466
3467
3468
3469
3470
3471
3472
3473
3474
3475
3476
3477
3478
3479
3480
3481
3482
3483
3484
3485
3486
3487
3488
3489
3490
3491
3492
3493
3494
3495
3496
3497
3498
3499
3500
3501
3502
3503
3504
3505
3506
3507
3508
3509
3510
3511
3512
3513
3514
3515
3516
3517
3518
3519
3520
3521
3522
3523
3524
3525
3526
3527
3528
3529
3530
3531
3532
3533
3534
3535
3536
3537
3538
3539
3540
3541
3542
3543
3544
3545
3546
3547
3548
3549
3550
3551
3552
3553
3554
3555
3556
3557
3558
3559
3560
3561
3562
# File 'lib/parse/query.rb', line 3379

def aggregate(pipeline, verbose: nil, mongo_direct: nil, rewrite_lookups: nil, raw_values: false, raw_field_names: false)
  validate_pipeline!(pipeline)

  # Auto-rewrite LLM-style $lookup stages against logical Parse class
  # names into the Parse-on-Mongo column form (_p_*/parseReference) when
  # the foreign class declares parse_reference. Idempotent on already-
  # rewritten input. Controlled by Parse.rewrite_lookups (default true)
  # or the per-call `rewrite_lookups:` kwarg.
  pipeline = Parse::LookupRewriter.auto_rewrite(
    pipeline, class_name: @table, enabled: rewrite_lookups,
  )

  # Automatically prepend query constraints as pipeline stages
  complete_pipeline = []
  lookup_stages = []  # Track if we have $inQuery constraints

  # Add $match stage from where constraints if any exist
  unless @where.empty?
    # `compile_where` is marker-free; `compile_markers` carries the
    # __aggregation_pipeline stages we need to extract below.
    where_clause = compile_where
    markers = compile_markers
    if where_clause.any? || markers.key?("__aggregation_pipeline")
      # Collect match conditions and stages
      initial_match_conditions = []
      aggregation_match_conditions = []
      non_match_stages = []
      post_lookup_match = {}

      # `where_clause` is already marker-free; treat as regular constraints.
      regular_constraints = where_clause

      if regular_constraints.any?
        # Handle dates first
        date_converted = convert_dates_for_aggregation(regular_constraints)

        # Extract $inQuery/$notInQuery and convert to $lookup stages
        if has_subquery_constraints?(date_converted)
          lookup_result = extract_subquery_to_lookup_stages(date_converted)
          date_converted = lookup_result[:constraints]
          lookup_stages = lookup_result[:lookup_stages]
          post_lookup_match = lookup_result[:post_lookup_match]
        end

        # Convert field names for aggregation context and handle pointers
        if date_converted.any?
          match_stage = convert_constraints_for_aggregation(date_converted)
          initial_match_conditions << match_stage
        end
      end

      # Extract aggregation pipeline stages from the marker view.
      if markers.key?("__aggregation_pipeline")
        markers["__aggregation_pipeline"].each do |stage|
          if stage.is_a?(Hash) && stage.key?("$match")
            aggregation_match_conditions << stage["$match"]
          else
            non_match_stages << stage
          end
        end
      end

      # Stage 1: Initial $match with regular constraints
      if initial_match_conditions.any?
        if initial_match_conditions.length == 1
          complete_pipeline << { "$match" => initial_match_conditions.first }
        else
          complete_pipeline << { "$match" => { "$and" => initial_match_conditions } }
        end
      end

      # Stage 2: $lookup stages for subqueries ($addFields, $lookup)
      if lookup_stages.any?
        lookup_stages.each do |stage|
          next if stage.key?("$project")
          complete_pipeline << stage
        end

        # Stage 3: Post-lookup $match
        if post_lookup_match.any?
          complete_pipeline << { "$match" => post_lookup_match }
        end

        # Note: Skip cleanup $project stage - see build_aggregation_pipeline for reasoning
      end

      # Stage 5: Aggregation $match conditions
      if aggregation_match_conditions.any?
        if aggregation_match_conditions.length == 1
          complete_pipeline << { "$match" => aggregation_match_conditions.first }
        else
          complete_pipeline << { "$match" => { "$and" => aggregation_match_conditions } }
        end
      end

      # Stage 6: Non-$match stages from aggregation pipeline
      complete_pipeline.concat(non_match_stages)
    end
  end

  # Append the provided pipeline stages
  complete_pipeline.concat(pipeline)

  # Add $sort stage from order constraints if any exist
  unless @order.empty?
    sort_stage = {}
    @order.each do |order_obj|
      # order_obj is a Parse::Order object with field and direction
      field_name = order_obj.field.to_s
      direction = order_obj.direction == :desc ? -1 : 1
      sort_stage[field_name] = direction
    end
    complete_pipeline << { "$sort" => sort_stage } if sort_stage.any?
  end

  # Add $skip stage if specified
  if @skip > 0
    complete_pipeline << { "$skip" => @skip }
  end

  # Add $limit stage if specified
  if @limit.is_a?(Numeric) && @limit > 0
    complete_pipeline << { "$limit" => @limit }
  end

  # Optimize pipeline by merging consecutive $match stages
  complete_pipeline = deduplicate_consecutive_match_stages(complete_pipeline)

  # Auto-detect whether this aggregation must run via the direct-MongoDB
  # path instead of Parse Server's REST /aggregate endpoint. Three
  # independent triggers, each of which REST /aggregate cannot serve:
  #
  #   * $inQuery / $notInQuery → $lookup stages (the original trigger).
  #   * An SDK-injected ACL $match on the internal _rperm / _wperm columns
  #     (readable_by / publicly_readable / writable_by and friends). Parse
  #     Server's REST aggregate rejects a $match on those columns.
  #   * A scoped query (session_token / scope_to_user / scope_to_role).
  #     REST /aggregate is master-key-only and enforces NEITHER ACL NOR
  #     CLP, so a scoped aggregate sent over REST silently runs unscoped
  #     as the master key — leaking sums/min/max/distinct over rows the
  #     caller cannot read. This is the same enforcement asymmetry the
  #     #distinct / #count / #results auto-routes already guard against;
  #     the scalar terminals (sum/average/min/max/count_distinct) all
  #     funnel through here, so routing them here fixes every one.
  #
  # `allow_internal_fields` is forwarded for internal-field pipelines: the
  # caller-supplied `pipeline` arg was validated above (line ~3343) with
  # the internal-fields denylist active, so any _rperm/_wperm reference in
  # the merged pipeline is provably SDK-injected, never user input.
  uses_internal_fields = pipeline_uses_internal_fields?(complete_pipeline)
  scoped = distinct_query_is_scoped?
  use_mongo_direct = mongo_direct
  if use_mongo_direct.nil?
    mongo_ready = defined?(Parse::MongoDB) && Parse::MongoDB.enabled?
    if lookup_stages && lookup_stages.any?
      use_mongo_direct = true if mongo_ready
    elsif scoped || uses_internal_fields
      if mongo_ready
        use_mongo_direct = true
      elsif scoped
        # Fail closed: a scoped aggregation cannot fall back to REST
        # /aggregate without silently bypassing ACL/CLP (master-key-only
        # endpoint). Refuse rather than leak unscoped results. Unscoped
        # internal-field pipelines keep the REST fallback (a master-key
        # correctness edge, not an enforcement bypass).
        raise_scoped_aggregation_requires_mongo_direct!
      end
    end
  end

  # When the pipeline is bound for direct MongoDB, translate every stage
  # through the direct-MongoDB field rewriter so user-supplied stages
  # (which use logical Parse field names like `$author`) reach the
  # correct on-disk columns (`$_p_author`). The Parse Server route does
  # not need this — Parse Server applies its own translation on the
  # aggregate endpoint — so the rewrite is gated on use_mongo_direct.
  if use_mongo_direct
    complete_pipeline = translate_pipeline_for_direct_mongodb(complete_pipeline)
  end

  Aggregation.new(self, complete_pipeline, verbose: verbose, mongo_direct: use_mongo_direct || false,
                  allow_internal_fields: uses_internal_fields,
                  raw_values: raw_values, raw_field_names: raw_field_names)
end

#aggregate_from_query(additional_stages = [], verbose: nil, mongo_direct: nil) ⇒ Aggregation

Converts the current query into an aggregate pipeline and executes it. This method automatically converts all query constraints (where, order, limit, skip, etc.) into MongoDB aggregation pipeline stages.

Examples:

# Convert a regular query to aggregate pipeline
query = User.where(:age.gte => 18).order(:name).limit(10)
aggregation = query.aggregate_from_query
results = aggregation.results

# With additional pipeline stages
aggregation = query.aggregate_from_query([
  { "$group" => { "_id" => "$department", "count" => { "$sum" => 1 } } }
])

Parameters:

  • additional_stages (Array<Hash>) (defaults to: [])

    optional additional pipeline stages to append

  • verbose (Boolean) (defaults to: nil)

    whether to print verbose debug output for the aggregation

Returns:

  • (Aggregation)

    an aggregation object that can be executed



3623
3624
3625
3626
3627
3628
3629
3630
3631
3632
3633
3634
3635
3636
3637
3638
3639
3640
3641
3642
3643
3644
3645
3646
3647
3648
3649
3650
3651
3652
3653
3654
3655
3656
3657
3658
3659
3660
3661
# File 'lib/parse/query.rb', line 3623

def aggregate_from_query(additional_stages = [], verbose: nil, mongo_direct: nil)
  # Build pipeline from current query constraints
  pipeline, has_lookup_stages = build_query_aggregate_pipeline

  # `allow_internal_fields` is computed from the SDK-built portion ONLY
  # (before appending caller stages): build_query_aggregate_pipeline emits
  # the _rperm/_wperm $match for readable_by/etc., but `additional_stages`
  # is caller-supplied and NOT validated here, so we must not sanction an
  # internal-field reference the caller smuggled in. A scoped query still
  # routes to mongo-direct regardless (so ACL/CLP enforcement runs).
  uses_internal_fields = pipeline_uses_internal_fields?(pipeline)

  # Append any additional stages
  pipeline.concat(additional_stages) if additional_stages.any?

  # Same routing contract as #aggregate: $lookup subqueries, an SDK ACL
  # $match, or a scoped query each require the direct-MongoDB path (REST
  # /aggregate cannot express _rperm/_wperm and is master-key-only/
  # unenforced). A scoped query fails closed when mongo-direct is
  # unavailable rather than silently running unscoped as master.
  scoped = distinct_query_is_scoped?
  use_mongo_direct = mongo_direct
  if use_mongo_direct.nil?
    mongo_ready = defined?(Parse::MongoDB) && Parse::MongoDB.enabled?
    if has_lookup_stages
      use_mongo_direct = true if mongo_ready
    elsif scoped || uses_internal_fields
      if mongo_ready
        use_mongo_direct = true
      elsif scoped
        raise_scoped_aggregation_requires_mongo_direct!
      end
    end
  end

  # Create Aggregation directly to avoid double-applying constraints
  Aggregation.new(self, pipeline, verbose: verbose, mongo_direct: use_mongo_direct || false,
                  allow_internal_fields: uses_internal_fields)
end

#all(expressions = { limit: :max }) { ... } ⇒ Array<Hash>, Array<Parse::Object>

Similar to #results but takes an additional set of conditions to apply. This method helps support the use of class and instance level scopes.

Parameters:

  • expressions (Hash) (defaults to: { limit: :max })

    containing key value pairs of Parse::Operations and their value.

Yields:

  • a block to iterate for each object that matched the query.

Returns:

  • (Array<Hash>)

    if raw is set to true, a set of Parse JSON hashes.

  • (Array<Parse::Object>)

    if raw is set to false, a list of matching Parse::Object subclasses.

See Also:



4112
4113
4114
4115
4116
# File 'lib/parse/query.rb', line 4112

def all(expressions = { limit: :max }, &block)
  conditions(expressions)
  return results(&block) if block_given?
  results
end

#as_json(*args) ⇒ Hash

Returns:



4238
4239
4240
# File 'lib/parse/query.rb', line 4238

def as_json(*args)
  compile.as_json
end

#atlas_autocomplete(query, field:, **options) ⇒ Parse::AtlasSearch::AutocompleteResult

Execute an autocomplete search using MongoDB Atlas Search. Provides search-as-you-type functionality for a specific field.

Examples:

Basic autocomplete

result = Song.query.atlas_autocomplete("lov", field: :title)
result.suggestions # => ["Love Story", "Lovely Day", "Love Me Do"]

With fuzzy matching and filters

result = Song.query(:genre => "Pop").atlas_autocomplete("bea",
  field: :title,
  fuzzy: true,
  limit: 5
)

Parameters:

  • query (String)

    the partial search query (prefix)

  • field (Symbol, String)

    the field configured for autocomplete (required)

  • options (Hash)

    autocomplete options

Options Hash (**options):

  • :index (String)

    search index name (default: "default")

  • :fuzzy (Boolean)

    enable fuzzy matching (default: false)

  • :token_order (String)

    "any" or "sequential" (default: "any")

  • :limit (Integer)

    max suggestions to return (default: 10)

  • :raw (Boolean)

    return raw documents (default: false)

Returns:

Raises:

See Also:



2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
# File 'lib/parse/query.rb', line 2540

def atlas_autocomplete(query, field:, **options)
  require_relative "atlas_search"

  unless Parse::AtlasSearch.available?
    raise Parse::AtlasSearch::NotAvailable,
      "Atlas Search is not available. " \
      "Call Parse::AtlasSearch.configure(enabled: true) after configuring Parse::MongoDB."
  end

  # Merge query constraints as filter
  compiled_where = compile_where
  if compiled_where.present?
    regular_constraints = compiled_where.reject { |f, _| f == "__aggregation_pipeline" }
    options[:filter] = (options[:filter] || {}).merge(regular_constraints) if regular_constraints.any?
  end

  # Use query limit if set and no explicit limit provided
  options[:limit] ||= (@limit.is_a?(Numeric) && @limit > 0 ? @limit : 10)
  options[:class_name] = @table
  # Forward the query's read_preference (set via `#read_pref`).
  # See #atlas_search for the parity rationale.
  if @read_preference && !options.key?(:read_preference)
    options[:read_preference] = @read_preference
  end

  Parse::AtlasSearch.autocomplete(@table, query, field: field, **options)
end

#atlas_facets(query, facets, **options) ⇒ Parse::AtlasSearch::FacetedResult

Execute a faceted search using MongoDB Atlas Search. Returns search results along with aggregated facet counts for filtering.

Examples:

Faceted search by genre and decade

facets = {
  genre: { type: :string, path: :genre, num_buckets: 10 },
  decade: { type: :number, path: :year, boundaries: [1970, 1980, 1990, 2000, 2010] }
}
result = Song.query(:plays.gt => 100).atlas_facets("rock", facets)

result.total_count  # => 1500
result.facets[:genre]
# => [{ value: "Rock", count: 500 }, { value: "Pop Rock", count: 200 }, ...]

Parameters:

  • query (String, nil)

    the search query text (nil for match-all)

  • facets (Hash)

    facet definitions with the following structure:

    • name [Symbol] => Hash with:
      • :type [Symbol] - :string, :number, or :date
      • :path [Symbol, String] - the field path
      • :num_buckets [Integer] - (string only) max number of buckets
      • :boundaries [Array] - (number/date only) bucket boundaries
      • :default [String] - (number/date only) default bucket name
  • options (Hash)

    search options (same as atlas_search)

Returns:

Raises:

See Also:



2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
# File 'lib/parse/query.rb', line 2596

def atlas_facets(query, facets, **options)
  require_relative "atlas_search"

  unless Parse::AtlasSearch.available?
    raise Parse::AtlasSearch::NotAvailable,
      "Atlas Search is not available. " \
      "Call Parse::AtlasSearch.configure(enabled: true) after configuring Parse::MongoDB."
  end

  # Merge query constraints as filter
  compiled_where = compile_where
  if compiled_where.present?
    regular_constraints = compiled_where.reject { |f, _| f == "__aggregation_pipeline" }
    options[:filter] = (options[:filter] || {}).merge(regular_constraints) if regular_constraints.any?
  end

  # Use query limit/skip if set
  options[:limit] ||= (@limit.is_a?(Numeric) && @limit > 0 ? @limit : 100)
  options[:skip] ||= (@skip > 0 ? @skip : 0)
  options[:class_name] = @table
  # Forward the query's read_preference (set via `#read_pref`).
  # See #atlas_search for the parity rationale.
  if @read_preference && !options.key?(:read_preference)
    options[:read_preference] = @read_preference
  end

  Parse::AtlasSearch.faceted_search(@table, query, facets, **options)
end

#atlas_search(query = nil, **options) {|SearchBuilder| ... } ⇒ Parse::AtlasSearch::SearchResult

Execute a full-text search using MongoDB Atlas Search. Combines existing query constraints with Atlas Search capabilities.

Supports both simple options hash API and builder block for complex queries.

Examples:

Simple text search

songs = Song.query(:plays.gt => 1000).atlas_search("love ballad", fields: [:title, :lyrics])

With fuzzy matching

songs = Song.query.atlas_search("lvoe", fuzzy: true, limit: 20)

Complex search with builder block

songs = Song.query.atlas_search do |search|
  search.text(query: "love", path: [:title, :lyrics])
  search.phrase(query: "broken heart", path: :lyrics, slop: 2)
  search.with_highlight(path: :lyrics)
end

Parameters:

  • query (String, nil) (defaults to: nil)

    the search query text (required unless using block)

  • options (Hash)

    search options

Options Hash (**options):

  • :index (String)

    search index name (default: "default")

  • :fields (Array<String>, String, Symbol)

    fields to search

  • :fuzzy (Boolean)

    enable fuzzy matching (default: false)

  • :fuzzy_max_edits (Integer)

    max edit distance for fuzzy (1 or 2)

  • :highlight_field (Symbol, String)

    field to return highlights for

  • :limit (Integer)

    max results to return (overrides query limit)

  • :skip (Integer)

    number of results to skip (overrides query skip)

  • :raw (Boolean)

    return raw MongoDB documents (default: false)

Yields:

  • (SearchBuilder)

    optional block to configure complex search

Returns:

Raises:

See Also:



2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
# File 'lib/parse/query.rb', line 2430

def atlas_search(query = nil, **options, &block)
  require_relative "atlas_search"

  unless Parse::AtlasSearch.available?
    raise Parse::AtlasSearch::NotAvailable,
      "Atlas Search is not available. " \
      "Call Parse::AtlasSearch.configure(enabled: true) after configuring Parse::MongoDB."
  end

  # Determine limit and skip from query or options
  limit = options[:limit] || (@limit.is_a?(Numeric) && @limit > 0 ? @limit : 100)
  skip_val = options[:skip] || (@skip > 0 ? @skip : 0)

  if block_given?
    # Builder block mode
    index_name = options[:index] || Parse::AtlasSearch.default_index
    builder = Parse::AtlasSearch::SearchBuilder.new(index_name: index_name)
    yield builder

    # Build pipeline: $search must be first
    pipeline = [builder.build]

    # Add score projection
    pipeline << { "$addFields" => { "_score" => { "$meta" => "searchScore" } } }

    # Add existing query constraints as $match
    compiled_where = compile_where
    if compiled_where.present?
      regular_constraints = compiled_where.reject { |f, _| f == "__aggregation_pipeline" }
      if regular_constraints.any?
        mongo_constraints = convert_constraints_for_direct_mongodb(regular_constraints)
        pipeline << { "$match" => mongo_constraints }
      end
    end

    # Add sort, skip, limit
    pipeline << { "$sort" => { "_score" => -1 } }
    pipeline << { "$skip" => skip_val } if skip_val > 0
    pipeline << { "$limit" => limit }

    # SDK-built pipeline only — see results_direct for rationale.
    raw_results = Parse::MongoDB.aggregate(@table, pipeline,
                                           allow_internal_fields: true,
                                           read_preference: @read_preference,
                                         hint: @hint)

    # Convert results
    if options[:raw]
      Parse::AtlasSearch::SearchResult.new(results: raw_results, raw_results: raw_results)
    else
      parse_results = Parse::MongoDB.convert_documents_to_parse(raw_results, @table)
      objects = parse_results.map { |doc| Parse.decode(doc) }.compact
      Parse::AtlasSearch::SearchResult.new(results: objects, raw_results: raw_results)
    end
  else
    # Simple options API - delegate to AtlasSearch module
    raise ArgumentError, "query string is required when not using a block" if query.nil?

    # Merge query constraints as filter
    compiled_where = compile_where
    if compiled_where.present?
      regular_constraints = compiled_where.reject { |f, _| f == "__aggregation_pipeline" }
      options[:filter] = (options[:filter] || {}).merge(regular_constraints) if regular_constraints.any?
    end

    options[:class_name] = @table
    options[:limit] = limit
    options[:skip] = skip_val
    # Forward the query's read_preference (set via `#read_pref`).
    # Without this, Atlas Search calls reached through the Query
    # bridge silently fall back to the client default even though
    # the query explicitly opted in to a secondary read — the
    # mongo-direct path (`#results_direct`) honors it, this one
    # used to drop it on the floor.
    if @read_preference && !options.key?(:read_preference)
      options[:read_preference] = @read_preference
    end

    Parse::AtlasSearch.search(@table, query, **options)
  end
end

#average(field) ⇒ Float Also known as: avg

Calculate the average of values for a specific field.

Parameters:

Returns:

  • (Float)

    the average of all values for the field, or 0 if no results.



4371
4372
4373
4374
4375
4376
4377
4378
4379
4380
4381
4382
4383
4384
4385
# File 'lib/parse/query.rb', line 4371

def average(field)
  if field.nil? || !field.respond_to?(:to_s)
    raise ArgumentError, "Invalid field name passed to `average`."
  end

  # Format field name according to Parse conventions
  formatted_field = format_aggregation_field(field)

  # Build the aggregation pipeline
  pipeline = [
    { "$group" => { "_id" => nil, "avg" => { "$avg" => "$#{formatted_field}" } } },
  ]

  execute_basic_aggregation(pipeline, "average", field, "avg")
end

#before_prepare { ... } ⇒ Object

A callback called before the query is compiled

Yields:

  • A block to execute for the callback.

See Also:

  • ActiveModel::Callbacks


129
# File 'lib/parse/query.rb', line 129

define_model_callbacks :prepare, only: [:after, :before]

#build_aggregation_pipelineArray

Build the complete aggregation pipeline from constraints Pipeline order: $match (regular) -> $lookup (subqueries) -> $match (post-lookup) -> $match (aggregation) -> non-$match stages -> limit/skip

Returns:

  • (Array)

    Two element array: [pipeline, has_lookup_stages]



3855
3856
3857
3858
3859
3860
3861
3862
3863
3864
3865
3866
3867
3868
3869
3870
3871
3872
3873
3874
3875
3876
3877
3878
3879
3880
3881
3882
3883
3884
3885
3886
3887
3888
3889
3890
3891
3892
3893
3894
3895
3896
3897
3898
3899
3900
3901
3902
3903
3904
3905
3906
3907
3908
3909
3910
3911
3912
3913
3914
3915
3916
3917
3918
3919
3920
3921
3922
3923
3924
3925
3926
3927
3928
3929
3930
3931
3932
3933
3934
3935
3936
3937
3938
3939
3940
3941
3942
3943
3944
3945
3946
3947
3948
3949
3950
3951
3952
3953
3954
3955
3956
3957
3958
3959
3960
3961
3962
3963
# File 'lib/parse/query.rb', line 3855

def build_aggregation_pipeline
  pipeline = []
  # `compile_where` is already marker-free; `compile_markers` retains
  # the __aggregation_pipeline marker we need to extract stages from.
  compiled_where = compile_where
  markers = compile_markers
  has_lookup_stages = false

  # Collect match conditions and stages
  initial_match_conditions = []
  aggregation_match_conditions = []
  non_match_stages = []
  lookup_stages = []
  post_lookup_match = {}

  # `compiled_where` is already marker-free; use as-is.
  regular_constraints = compiled_where

  # Process regular constraints
  if regular_constraints.any?
    # Convert symbols to strings and handle date objects for MongoDB aggregation
    stringified_constraints = convert_dates_for_aggregation(JSON.parse(regular_constraints.to_json))

    # Extract $inQuery/$notInQuery and convert to $lookup stages
    if has_subquery_constraints?(stringified_constraints)
      lookup_result = extract_subquery_to_lookup_stages(stringified_constraints)
      stringified_constraints = lookup_result[:constraints]
      lookup_stages = lookup_result[:lookup_stages]
      post_lookup_match = lookup_result[:post_lookup_match]
      has_lookup_stages = lookup_stages.any?
    end

    # Convert remaining pointer field names and values to MongoDB aggregation format
    if stringified_constraints.any?
      stringified_constraints = convert_constraints_for_aggregation(stringified_constraints)
      initial_match_conditions << stringified_constraints
    end
  end

  # Extract aggregation pipeline stages (from empty_or_nil, set_equals, etc.)
  if markers.key?("__aggregation_pipeline")
    markers["__aggregation_pipeline"].each do |stage|
      if stage.is_a?(Hash) && stage.key?("$match")
        # Aggregation $match conditions go after lookup
        aggregation_match_conditions << stage["$match"]
      else
        # Non-$match stages go directly to pipeline
        non_match_stages << stage
      end
    end
  end

  # Stage 1: Initial $match with regular constraints (before lookup)
  # This filters down the dataset before the expensive $lookup
  if initial_match_conditions.any?
    if initial_match_conditions.length == 1
      pipeline << { "$match" => initial_match_conditions.first }
    else
      pipeline << { "$match" => { "$and" => initial_match_conditions } }
    end
  end

  # Stage 2: $lookup stages for subqueries ($addFields, $lookup)
  # These join with related collections and filter based on subquery conditions
  if lookup_stages.any?
    # Add $addFields and $lookup stages (skip $project stages)
    lookup_stages.each do |stage|
      next if stage.key?("$project")
      pipeline << stage
    end

    # Stage 3: Post-lookup $match to filter based on lookup results
    if post_lookup_match.any?
      pipeline << { "$match" => post_lookup_match }
    end

    # Note: We intentionally skip cleanup $project stage because:
    # 1. Parse Server's aggregation result processing ignores unknown fields
    # 2. Using $project with exclusions can cause issues in some MongoDB versions
    # 3. The temporary lookup fields (_lookup_*_id, _lookup_*_result) won't affect the output
  end

  # Stage 5: Aggregation $match conditions (from empty_or_nil, set_equals, etc.)
  if aggregation_match_conditions.any?
    if aggregation_match_conditions.length == 1
      pipeline << { "$match" => aggregation_match_conditions.first }
    else
      pipeline << { "$match" => { "$and" => aggregation_match_conditions } }
    end
  end

  # Stage 6: Non-$match stages from aggregation pipeline
  pipeline.concat(non_match_stages)

  # Stage 7: Add limit if specified
  if @limit.is_a?(Numeric) && @limit > 0
    pipeline << { "$limit" => @limit }
  end

  # Stage 8: Add skip if specified
  if @skip > 0
    pipeline << { "$skip" => @skip }
  end

  # Optimize pipeline by merging consecutive $match stages
  pipeline = deduplicate_consecutive_match_stages(pipeline)

  [pipeline, has_lookup_stages]
end

#build_direct_mongodb_pipelineArray<Hash>

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Build an aggregation pipeline optimized for direct MongoDB execution. This differs from build_aggregation_pipeline in that it uses MongoDB's native field names (id, _created_at, _updated_at, _p* for pointers).

Returns:

  • (Array<Hash>)

    MongoDB aggregation pipeline stages



2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
# File 'lib/parse/query.rb', line 2631

def build_direct_mongodb_pipeline
  pipeline = []

  # Mirror the REST compile() behavior: ensure each top-level included field
  # is also in @keys so the $project stage below does not strip the pointer
  # that the $lookup stage is supposed to expand.
  merge_includes_into_keys!

  # Compile the where clause and convert for direct MongoDB access.
  # `compile_where` already strips `__`-prefixed routing markers; use
  # `compile_markers` to recover the unfiltered hash for the
  # __aggregation_pipeline extraction below.
  compiled_where = compile_where
  markers = compile_markers

  # Note: the `_rperm` injection for scope_to_user no longer
  # happens here. It moved to Parse::MongoDB.aggregate via the
  # acl_user: kwarg so the same three-layer ACL simulation
  # (top-level $match + $lookup rewriter + post-fetch redactor)
  # runs for scope_to_user, session_token, and the public-only
  # fallback alike. See {#mongo_direct_auth_kwargs}.

  if compiled_where.present?
    # Convert field names and values for direct MongoDB access.
    # `compiled_where` is already marker-free, so no further
    # reject pass is required.
    mongo_constraints = convert_constraints_for_direct_mongodb(compiled_where)
    pipeline << { "$match" => mongo_constraints } if mongo_constraints.any?
  end

  # Handle aggregation pipeline stages (from empty_or_nil, set_equals, etc.)
  if markers.key?("__aggregation_pipeline")
    markers["__aggregation_pipeline"].each do |stage|
      pipeline << convert_stage_for_direct_mongodb(stage)
    end
  end

  # Add sort stage if order is specified
  if @order.any?
    sort_spec = {}
    @order.each do |order_clause|
      # Handle both Parse::Order objects and string representations
      if order_clause.is_a?(Parse::Order)
        field = order_clause.field.to_s
        direction = order_clause.direction == :desc ? -1 : 1
        sort_spec[convert_field_for_direct_mongodb(field)] = direction
      elsif order_clause.is_a?(String)
        # Parse order clause (e.g., "-createdAt" or "name")
        if order_clause.start_with?("-")
          field = order_clause[1..-1]
          sort_spec[convert_field_for_direct_mongodb(field)] = -1
        else
          sort_spec[convert_field_for_direct_mongodb(order_clause)] = 1
        end
      end
    end
    pipeline << { "$sort" => sort_spec } if sort_spec.any?
  end

  # Add include/eager loading $lookup stages if @includes is populated
  # These stages resolve pointer fields to full objects
  if @includes.any?
    include_stages = build_include_lookup_stages(@includes)
    pipeline.concat(include_stages)
  end

  # Add skip stage if specified
  pipeline << { "$skip" => @skip } if @skip > 0

  # Add limit stage if specified
  pipeline << { "$limit" => @limit } if @limit.is_a?(Numeric) && @limit > 0

  # Add $project stage if specific keys are requested
  # Always include required fields: _id, _created_at, _updated_at, _acl
  if @keys.any?
    project_stage = {
      "_id" => 1,
      "_created_at" => 1,
      "_updated_at" => 1,
      "_acl" => 1,
    }
    @keys.each do |key|
      mongo_field = convert_field_for_direct_mongodb(key.to_s)
      project_stage[mongo_field] = 1
    end
    pipeline << { "$project" => project_stage }
  end

  # Optimize pipeline by merging consecutive $match stages
  deduplicate_consecutive_match_stages(pipeline)
end

#build_filter_condition(where) ⇒ Hash

Build a $filter condition expression from where constraints

Parameters:

  • where (Hash)

    the where constraints

Returns:

  • (Hash)

    MongoDB expression for $filter cond



4064
4065
4066
4067
4068
4069
4070
4071
4072
4073
4074
4075
4076
4077
4078
4079
4080
4081
4082
# File 'lib/parse/query.rb', line 4064

def build_filter_condition(where)
  conditions = where.map do |field, value|
    if value.is_a?(Hash)
      # Handle operators like $gt, $lt, etc.
      value.map do |op, val|
        { op => ["$$item.#{field}", val] }
      end
    else
      # Simple equality
      { "$eq" => ["$$item.#{field}", value] }
    end
  end.flatten

  if conditions.length == 1
    conditions.first
  else
    { "$and" => conditions }
  end
end

#build_include_lookup_stages(includes) ⇒ Array<Hash>

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Build $lookup stages for included pointer fields in direct MongoDB queries. This enables eager loading of related objects when using results_direct.

Parameters:

  • includes (Array<Symbol>)

    the fields to include (from @includes)

Returns:

  • (Array<Hash>)

    MongoDB $lookup stages for each included field



2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
# File 'lib/parse/query.rb', line 2729

def build_include_lookup_stages(includes)
  return [] if includes.nil? || includes.empty?

  stages = []
  includes.each do |field|
    # Handle nested includes (e.g., 'artist.label') - only process first level
    field_str = field.to_s
    base_field = field_str.split(".").first.to_sym

    # Get target class from model references
    target_class = get_pointer_target_class(base_field)
    next unless target_class

    # MongoDB pointer field name
    mongo_pointer_field = "_p_#{base_field}"
    lookup_result_field = "_included_#{base_field}"
    lookup_id_field = "_include_id_#{base_field}"

    # Stage 1: Extract objectId from pointer string using $split
    # Parse pointers are stored as "ClassName$objectId"
    stages << {
      "$addFields" => {
        lookup_id_field => {
          "$arrayElemAt" => [
            { "$split" => ["$#{mongo_pointer_field}", { "$literal" => "$" }] },
            1,
          ],
        },
      },
    }

    # Stage 2: $lookup to join with target collection
    stages << {
      "$lookup" => {
        "from" => target_class,
        "localField" => lookup_id_field,
        "foreignField" => "_id",
        "as" => lookup_result_field,
      },
    }

    # Stage 3: Unwind the array (since $lookup returns array, but we want single object)
    stages << {
      "$unwind" => {
        "path" => "$#{lookup_result_field}",
        "preserveNullAndEmptyArrays" => true,
      },
    }

    # Stage 4: Clean up temporary lookup ID field
    stages << {
      "$unset" => lookup_id_field,
    }
  end

  stages
end

#clause(clause_name = :where) ⇒ Object

returns the query clause for the particular clause

Parameters:

  • clause_name (Symbol) (defaults to: :where)

    One of supported clauses to return: :keys, :where, :order, :includes, :limit, :skip

Returns:

  • (Object)

    the content of the clause for this query.



601
602
603
604
# File 'lib/parse/query.rb', line 601

def clause(clause_name = :where)
  return unless [:keys, :where, :order, :includes, :limit, :skip].include?(clause_name)
  instance_variable_get "@#{clause_name}".to_sym
end

#clear(item = :results) ⇒ self

Clear a specific clause of this query. This can be one of: :where, :order, :includes, :skip, :limit, :count, :keys or :results.

Parameters:

  • item (:Symbol) (defaults to: :results)

    the clause to clear.

Returns:

  • (self)


447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
# File 'lib/parse/query.rb', line 447

def clear(item = :results)
  case item
  when :where
    # an array of Parse::Constraint subclasses
    @where = []
  when :order
    # an array of Parse::Order objects
    @order = []
  when :includes
    @includes = []
  when :skip
    @skip = 0
  when :limit
    @limit = nil
  when :count
    @count = 0
  when :keys
    @keys = []
  end
  @results = nil
  self # chaining
end

#cloneParse::Query

Note:

The @client and @results instance variables are intentionally NOT cloned. The cloned query will use the default client when executed.

Creates a deep copy of this query object, allowing independent modifications

Returns:

  • (Parse::Query)

    a new query object with the same constraints



5550
5551
5552
5553
5554
5555
5556
5557
5558
5559
5560
5561
5562
5563
5564
5565
5566
5567
5568
5569
5570
5571
5572
5573
5574
# File 'lib/parse/query.rb', line 5550

def clone
  cloned_query = Parse::Query.new(self.instance_variable_get(:@table))
  # Note: :client is intentionally excluded - it contains non-serializable objects
  # (Redis connections, Faraday connections) and should be obtained lazily
  [:count, :where, :order, :keys, :exclude_keys, :includes, :limit, :skip, :cache, :use_master_key, :hint].each do |param|
    if instance_variable_defined?(:"@#{param}")
      value = instance_variable_get(:"@#{param}")
      if value.is_a?(Array) || value.is_a?(Hash)
        # Use Marshal for deep copy of complex constraint objects
        begin
          cloned_value = Marshal.load(Marshal.dump(value))
        rescue => e
          # Fallback to shallow copy if Marshal fails
          puts "[Parse::Query.clone] Marshal failed for #{param}: #{e.message}, falling back to dup"
          cloned_value = value.dup
        end
      else
        cloned_value = value
      end
      cloned_query.instance_variable_set(:"@#{param}", cloned_value)
    end
  end
  cloned_query.instance_variable_set(:@results, nil)
  cloned_query
end

#compile(encode: true, includeClassName: false) ⇒ Hash

Complies the query and runs all prepare callbacks.

Parameters:

  • encode (Boolean) (defaults to: true)

    whether to encode the where clause to a JSON string.

  • includeClassName (Boolean) (defaults to: false)

    whether to include the class name of the collection.

Returns:

  • (Hash)

    a hash representing the prepared query request.

See Also:



4256
4257
4258
4259
4260
4261
4262
4263
4264
4265
4266
4267
4268
4269
4270
4271
4272
4273
4274
4275
4276
4277
4278
4279
4280
4281
4282
4283
4284
4285
4286
4287
4288
4289
4290
4291
4292
4293
4294
4295
4296
4297
4298
4299
4300
4301
# File 'lib/parse/query.rb', line 4256

def compile(encode: true, includeClassName: false)
  # Validate includes vs keys before compiling
  validate_includes_vs_keys

  # When a `keys` allowlist is set alongside `include`, the parent pointer
  # field must also be in `keys` or Parse Server strips it before expanding
  # the include. Auto-add the top-level segment of each include so partial
  # fetches don't silently drop included pointers.
  merge_includes_into_keys!

  run_callbacks :prepare do
    q = {} #query
    q[:limit] = @limit if @limit.is_a?(Numeric) && @limit > 0
    q[:skip] = @skip if @skip > 0

    q[:include] = @includes.join(",") unless @includes.empty?
    q[:keys] = @keys.join(",") unless @keys.empty?
    q[:excludeKeys] = @exclude_keys.join(",") if encode && @exclude_keys&.any?
    q[:order] = @order.join(",") unless @order.empty?
    unless @where.empty?
      q[:where] = Parse::Query.compile_where(@where)
      q[:where] = q[:where].to_json if encode
    end

    if @count && @count > 0
      # if count is requested
      q[:limit] = 0
      q[:count] = 1
    end
    # Read preference must ride the REST query body (restOptions), NOT a
    # header: Parse Server's middleware does not map any
    # `X-Parse-Read-Preference` header into request options, so the
    # header alone is silently ignored and the read always hits the
    # primary. `RestQuery` reads `readPreference` from restOptions, so
    # emitting it here is what actually routes the read. (The header is
    # still sent for any intermediary that honors it; it is harmless.)
    if encode && (pref = normalized_read_preference)
      q[:readPreference] = pref
    end
    q[:hint] = @hint if @hint
    if includeClassName
      q[:className] = @table
    end
    q
  end
end

#compile_whereHash

Returns a hash representing just the where clause of this query, with SDK-internal routing markers stripped.

Returns:

  • (Hash)

    a hash representing just the where clause of this query, with SDK-internal routing markers stripped.



4305
4306
4307
# File 'lib/parse/query.rb', line 4305

def compile_where
  self.class.compile_where(@where || [])
end

#conditions(expressions = {}) ⇒ self Also known as: query, append

Add a set of query expressions and constraints.

Examples:

query.conditions({:field.gt => value})

Parameters:

  • expressions (Hash) (defaults to: {})

    containing key value pairs of Parse::Operations and their value.

Returns:

  • (self)


522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
# File 'lib/parse/query.rb', line 522

def conditions(expressions = {})
  expressions.each do |expression, value|
    # Normalize to symbol for comparison (handles both string and symbol keys)
    expr_sym = expression.respond_to?(:to_sym) ? expression.to_sym : expression

    if expr_sym == :order
      order value
    elsif expr_sym == :keys
      keys value
    elsif expr_sym == :key
      keys [value]
    elsif expr_sym == :skip
      skip value
    elsif expr_sym == :limit
      limit value
    elsif expr_sym == :include || expr_sym == :includes
      includes(value)
    elsif expr_sym == :cache
      self.cache = value
    elsif expr_sym == :use_master_key
      self.use_master_key = value
    elsif expr_sym == :session
      # you can pass a session token or a Parse::Session
      self.session_token = value
    elsif expr_sym == :read_preference
      self.read_preference = value
      # ACL convenience query options
    elsif expr_sym == :readable_by
      readable_by(value)
    elsif expr_sym == :writable_by
      writable_by(value)
    elsif expr_sym == :readable_by_role
      readable_by_role(value)
    elsif expr_sym == :writable_by_role
      writable_by_role(value)
    elsif expr_sym == :publicly_readable
      publicly_readable if value
    elsif expr_sym == :publicly_writable
      publicly_writable if value
    elsif expr_sym == :privately_readable || expr_sym == :master_key_read_only
      privately_readable if value
    elsif expr_sym == :privately_writable || expr_sym == :master_key_write_only
      privately_writable if value
    elsif expr_sym == :private_acl || expr_sym == :master_key_only
      private_acl if value
    elsif expr_sym == :not_publicly_readable
      not_publicly_readable if value
    elsif expr_sym == :not_publicly_writable
      not_publicly_writable if value
    else
      add_constraint(expression, value)
    end
  end # each
  self #chaining
end

#constraints(raw = false) ⇒ Array<Parse::Constraint>, Hash

Parameters:

  • raw (Boolean) (defaults to: false)

    whether to return the hash form of the constraints.

Returns:

  • (Array<Parse::Constraint>)

    if raw is false, an array of constraints composing the :where clause for this query.

  • (Hash)

    if raw is true, a hash representing the constraints.



1011
1012
1013
# File 'lib/parse/query.rb', line 1011

def constraints(raw = false)
  raw ? where_constraints : @where
end

#convert_addfields_for_direct_mongodb(spec) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Convert a $addFields / $set stage for direct MongoDB. Same shape as $project: { aliasName => <expression> }. Output aliases pass through verbatim; each value is walked as an aggregation expression so storage-column references inside reach the correct column via the schema-aware #convert_field_for_direct_mongodb.



3131
3132
3133
3134
3135
3136
3137
3138
3139
# File 'lib/parse/query.rb', line 3131

def convert_addfields_for_direct_mongodb(spec)
  return spec unless spec.is_a?(Hash)

  result = {}
  spec.each do |field, value|
    result[field] = rewrite_expression_for_direct_mongodb(value)
  end
  result
end

#convert_constraints_for_direct_mongodb(constraints) ⇒ Hash

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Convert constraints for direct MongoDB execution.

Parameters:

  • constraints (Hash)

    the compiled where constraints

Returns:

  • (Hash)

    constraints with MongoDB-native field names



2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843
2844
2845
2846
2847
2848
2849
2850
2851
2852
2853
2854
2855
2856
2857
2858
2859
2860
2861
2862
2863
2864
2865
2866
2867
2868
2869
# File 'lib/parse/query.rb', line 2823

def convert_constraints_for_direct_mongodb(constraints)
  return constraints unless constraints.is_a?(Hash)

  # $relatedTo resolves a Parse Relation, which is stored in the
  # `_Join:<key>:<ParentClass>` collection — a join the SDK does NOT
  # translate on the mongo-direct path. Passed through verbatim it reaches
  # MongoDB as an unknown `$match` operator and fails with an opaque error;
  # and any future attempt to rewrite it into a `$lookup` would have to
  # re-implement the `_rperm` / protectedFields enforcement that the rest of
  # this path applies post-fetch. Parse Server's own `$relatedTo` was found
  # to bypass exactly that enforcement (GHSA-wmwx-jr2p-4j4r), so fail closed
  # here with a clear message rather than risk a silent leak: this query
  # must run via REST (the default), where Parse Server resolves the
  # relation under its own ACL / CLP enforcement.
  if constraints.key?("$relatedTo") || constraints.key?(:"$relatedTo")
    raise ArgumentError,
      "[Parse::Query] $relatedTo cannot run on the mongo-direct path; a " \
      "Parse Relation is resolved server-side via its join collection. Run " \
      "this query via REST (omit `mongo_direct:` / `.results_direct` and any " \
      "direct-only constraint), or express the membership as an `$inQuery` " \
      "against the relation's join collection."
  end

  result = {}
  constraints.each do |field, value|
    field_str = field.to_s

    # Skip special operators
    if field_str.start_with?("$")
      # Recursively convert nested constraints in $and, $or, $nor
      if value.is_a?(Array) && %w[$and $or $nor].include?(field_str)
        result[field_str] = value.map { |v| convert_constraints_for_direct_mongodb(v) }
      else
        result[field_str] = value
      end
      next
    end

    # Convert field name for MongoDB
    mongo_field = convert_field_for_direct_mongodb(field_str)

    # Convert value
    result[mongo_field] = convert_value_for_direct_mongodb(field_str, value)
  end

  result
end

#convert_field_for_direct_mongodb(field) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Convert a field name for direct MongoDB access.

Parameters:

  • field (String)

    the Parse field name

Returns:

  • (String)

    the MongoDB field name



2875
2876
2877
2878
2879
2880
2881
2882
2883
2884
2885
2886
2887
2888
2889
2890
2891
2892
2893
2894
2895
2896
2897
2898
2899
2900
2901
2902
2903
2904
2905
2906
2907
2908
2909
2910
2911
2912
2913
2914
2915
2916
2917
2918
2919
2920
2921
2922
2923
2924
2925
2926
2927
2928
2929
2930
2931
2932
2933
2934
2935
2936
2937
2938
2939
2940
2941
2942
2943
2944
2945
2946
2947
2948
2949
2950
2951
2952
2953
2954
2955
2956
2957
2958
2959
# File 'lib/parse/query.rb', line 2875

def convert_field_for_direct_mongodb(field)
  field_str = field.to_s

  # Any field name starting with underscore is non-user-facing and is
  # passed through verbatim. Parse user-facing properties never start
  # with `_` (the SDK columnizes snake_case to camelCase before save,
  # and Parse Server reserves the leading-underscore namespace), so a
  # field that does is one of:
  #   - a MongoDB/Parse Server internal column (`_id`, `_created_at`,
  #     `_acl`, `_rperm`, `_wperm`, `_hashed_password`,
  #     `_session_token`, `_email_verify_token`, ...)
  #   - a Parse-on-Mongo pointer storage column (`_p_<field>`)
  #   - an SDK-built pipeline-temp alias such as the
  #     `_lookup_<field>_result` / `_lookup_<field>_id` aliases that
  #     `extract_subquery_to_lookup_stages` introduces when an
  #     `$inQuery` constraint compiles to a `$lookup` stage
  # Columnizing any of these would corrupt the reference: the
  # previous behavior of routing `_lookup_project_result` through
  # `format_field` produced `lookupProjectResult` (leading underscore
  # stripped, snake_case to camelCase), and the post-lookup
  # `$match` then asked MongoDB for a column that didn't exist, so
  # every document silently satisfied the constraint.
  return field_str if field_str.start_with?("_")

  # Apply field formatting for regular fields
  formatted = Query.format_field(field)

  case formatted
  when "objectId"
    "_id"
  when "createdAt"
    "_created_at"
  when "updatedAt"
    "_updated_at"
  else
    # Schema-aware passthrough: only rewrite names that correspond
    # to a declared Parse property (or the universal built-ins
    # handled above). Anything else is treated as a pipeline-local
    # alias — `$group` accumulator name, `$project` computed field,
    # `$addFields` output — and the literal text passes through so
    # the reference matches the output key the upstream stage
    # produced.
    #
    # Concretely: `$status` on a class that declares `status`
    # remains `status` (`format_field` is a no-op for already-
    # camelCase names); `$author` on a class that declares a
    # pointer `author` becomes `$_p_author`; `$contributor_set`
    # (an alias the caller introduced in `$group`) stays
    # `$contributor_set` because no such property exists in the
    # schema. Callers reading the result row by `row[alias_name]`
    # see exactly the spelling they wrote into the pipeline.
    #
    # @note Two documented limitations of the schema-aware rule:
    #
    # 1. **Alias shadowing.** An alias whose name shadows a
    #    declared Parse property (`$group { author: ... }` where
    #    `author` is a pointer) is treated as the property —
    #    downstream `$author` references resolve to `$_p_author`,
    #    the storage column, not the alias. Avoid alias names that
    #    collide with declared property names. The same naming
    #    constraint MongoDB aggregation pipelines have generally;
    #    not unique to parse-stack.
    #
    # 2. **Undeclared server columns.** Conversely, a `$field`
    #    reference whose name corresponds to a column that exists
    #    on the server but is NOT declared as a property on the
    #    Ruby model passes through verbatim. The schema we consult
    #    is the SDK-side property registry; we do not introspect
    #    the live server schema on every translation. If you need
    #    references in mongo-direct pipelines to translate
    #    snake_case → camelCase or take a `_p_*` prefix, declare
    #    the corresponding property on the Ruby model. Workaround
    #    without declaring: write the storage-column name directly
    #    (`$_p_author`, `$companyName`), which short-circuits the
    #    walker via the leading-underscore / already-formatted
    #    paths.
    return field_str unless field_is_known_to_schema?(formatted)

    if field_is_pointer?(formatted)
      "_p_#{formatted}"
    else
      formatted
    end
  end
end

#convert_group_for_direct_mongodb(group) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Convert $group stage for direct MongoDB. Output-alias keys (_id, accumulator names like contributor_set) pass through verbatim so the result row uses whatever spelling the caller wrote. Each value — the _id group-key expression and every accumulator expression — is walked as an aggregation expression so $field references inside reach the correct storage column (_p_* for pointers, _id/_created_at/_updated_at for built-ins, untouched for unknown names i.e. pipeline-local aliases) via the schema-aware #convert_field_for_direct_mongodb.



3115
3116
3117
3118
3119
3120
3121
3122
3123
# File 'lib/parse/query.rb', line 3115

def convert_group_for_direct_mongodb(group)
  return group unless group.is_a?(Hash)

  result = {}
  group.each do |field, value|
    result[field] = rewrite_expression_for_direct_mongodb(value)
  end
  result
end

#convert_match_for_direct_mongodb(match) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Convert a $match stage for direct MongoDB. Rewrites top-level field-name keys via #convert_constraints_for_direct_mongodb and additionally walks the value of a top-level $expr as an aggregation expression so nested $fieldName references are rewritten.



3059
3060
3061
3062
3063
3064
3065
3066
3067
3068
3069
3070
3071
3072
# File 'lib/parse/query.rb', line 3059

def convert_match_for_direct_mongodb(match)
  converted = convert_constraints_for_direct_mongodb(match)
  return converted unless converted.is_a?(Hash)

  # The constraint converter passes $expr through unchanged. Rewrite
  # its value here so e.g. {$expr: {$eq: ["$author", "$approver"]}}
  # becomes {$expr: {$eq: ["$_p_author", "$_p_approver"]}}.
  expr_key = converted.key?("$expr") ? "$expr" : (converted.key?(:"$expr") ? :"$expr" : nil)
  return converted unless expr_key

  result = converted.dup
  result[expr_key] = rewrite_expression_for_direct_mongodb(converted[expr_key])
  result
end

#convert_projection_for_direct_mongodb(projection) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Convert projection fields for direct MongoDB. Output-key aliases pass through verbatim — what the caller writes is what the result row will be keyed by. Values that are aggregation expressions (e.g. { "$cond": [...] }) are walked recursively so nested $fieldName references reach the correct storage column via the schema-aware rewriter in #convert_field_for_direct_mongodb.



3081
3082
3083
3084
3085
3086
3087
3088
3089
# File 'lib/parse/query.rb', line 3081

def convert_projection_for_direct_mongodb(projection)
  return projection unless projection.is_a?(Hash)

  result = {}
  projection.each do |field, value|
    result[field] = rewrite_expression_for_direct_mongodb(value)
  end
  result
end

#convert_replace_root_for_direct_mongodb(spec) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Convert a $replaceRoot stage for direct MongoDB. Argument shape is { newRoot: <expression> }; only the newRoot value is an expression. (Use #rewrite_expression_for_direct_mongodb directly for $replaceWith, whose argument is the expression itself.)



3146
3147
3148
3149
3150
3151
3152
3153
3154
3155
# File 'lib/parse/query.rb', line 3146

def convert_replace_root_for_direct_mongodb(spec)
  return rewrite_expression_for_direct_mongodb(spec) unless spec.is_a?(Hash)

  new_root_key = spec.key?("newRoot") ? "newRoot" : (spec.key?(:newRoot) ? :newRoot : nil)
  return rewrite_expression_for_direct_mongodb(spec) unless new_root_key

  result = spec.dup
  result[new_root_key] = rewrite_expression_for_direct_mongodb(spec[new_root_key])
  result
end

#convert_sort_for_direct_mongodb(sort) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Convert sort specification for direct MongoDB.



3093
3094
3095
3096
3097
3098
3099
3100
3101
3102
# File 'lib/parse/query.rb', line 3093

def convert_sort_for_direct_mongodb(sort)
  return sort unless sort.is_a?(Hash)

  result = {}
  sort.each do |field, direction|
    mongo_field = convert_field_for_direct_mongodb(field)
    result[mongo_field] = direction
  end
  result
end

#convert_stage_for_direct_mongodb(stage) ⇒ Hash

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Convert an aggregation stage for direct MongoDB execution.

Projection-shape stages ($project, $addFields, $set, $replaceRoot, $replaceWith) and accumulator/grouping stages ($group) carry aggregation expressions that can reference fields via $fieldName strings. These references must be rewritten to the direct-MongoDB column form (camelCase, p* for pointers, _id/_created_at/_updated_at for built-ins). The rewrite walks recursively into $cond / $eq / $switch / $expr argument arrays so a nested reference is not missed. See #rewrite_expression_for_direct_mongodb.

$match is special: its top-level keys are field-name constraints (rewritten via the constraint converter), but the value of a top-level $expr is an aggregation expression that must also be walked.

Parameters:

  • stage (Hash)

    a single pipeline stage

Returns:

  • (Hash)

    the converted stage



3026
3027
3028
3029
3030
3031
3032
3033
3034
3035
3036
3037
3038
3039
3040
3041
3042
3043
3044
3045
3046
3047
3048
3049
3050
3051
3052
# File 'lib/parse/query.rb', line 3026

def convert_stage_for_direct_mongodb(stage)
  return stage unless stage.is_a?(Hash)

  result = {}
  stage.each do |operator, value|
    case operator.to_s
    when "$match"
      result[operator] = convert_match_for_direct_mongodb(value)
    when "$project"
      result[operator] = convert_projection_for_direct_mongodb(value)
    when "$sort"
      result[operator] = convert_sort_for_direct_mongodb(value)
    when "$group"
      result[operator] = convert_group_for_direct_mongodb(value)
    when "$addFields", "$set"
      result[operator] = convert_addfields_for_direct_mongodb(value)
    when "$replaceRoot"
      result[operator] = convert_replace_root_for_direct_mongodb(value)
    when "$replaceWith"
      # $replaceWith's argument is the new-root expression directly.
      result[operator] = rewrite_expression_for_direct_mongodb(value)
    else
      result[operator] = value
    end
  end
  result
end

#convert_value_for_direct_mongodb(field, value) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Convert a value for direct MongoDB execution.

Parameters:

  • field (String)

    the field name (for context)

  • value (Object)

    the value to convert

Returns:

  • (Object)

    the converted value



2966
2967
2968
2969
2970
2971
2972
2973
2974
2975
2976
2977
2978
2979
2980
2981
2982
2983
2984
2985
2986
2987
2988
2989
2990
2991
2992
2993
2994
2995
2996
2997
2998
2999
3000
3001
3002
3003
3004
3005
3006
3007
# File 'lib/parse/query.rb', line 2966

def convert_value_for_direct_mongodb(field, value)
  case value
  when Hash
    # Handle both string and symbol keys for __type checks
    type_value = value["__type"] || value[:__type]

    if type_value == "Pointer"
      # Convert Parse pointer to MongoDB pointer string format
      class_name = value["className"] || value[:className]
      object_id = value["objectId"] || value[:objectId]
      "#{class_name}$#{object_id}"
    elsif type_value == "Date"
      # Convert Parse Date format to Time object for BSON Date
      iso_value = value["iso"] || value[:iso]
      Time.parse(iso_value).utc
    else
      # Recursively convert nested hash (for operators like $gt, $in, etc.)
      # Convert symbol keys to strings for MongoDB
      converted = {}
      value.each do |k, v|
        key_str = k.to_s
        converted[key_str] = convert_value_for_direct_mongodb(field, v)
      end
      converted
    end
  when Parse::Pointer
    "#{value.parse_class}$#{value.id}"
  when Parse::Date
    # Parse::Date extends DateTime - convert to Time for BSON Date
    value.to_time.utc
  when Time
    value.utc
  when DateTime
    value.to_time.utc
  when Date
    value.to_time.utc
  when Array
    value.map { |v| convert_value_for_direct_mongodb(field, v) }
  else
    value
  end
end

#count(mongo_direct: false) ⇒ Integer

Perform a count query.

Examples:

# get number of songs with a play_count > 10
Song.count :play_count.gt => 10

# same
query = Parse::Query.new("Song")
query.where :play_count.gt => 10
query.count

Parameters:

  • mongo_direct (Boolean) (defaults to: false)

    if true, queries MongoDB directly bypassing Parse Server. Requires Parse::MongoDB to be configured. Default: false.

Returns:

  • (Integer)

    the count result



1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
# File 'lib/parse/query.rb', line 1238

def count(mongo_direct: false)
  # Use direct MongoDB query if requested
  return count_direct if mongo_direct

  # Auto-route to mongo-direct when the compiled where contains a
  # direct-only constraint. Same gate as #results.
  if requires_mongo_direct?
    assert_mongo_direct_routable!
    return count_direct(**mongo_direct_auth_kwargs)
  end

  # Check if this query requires aggregation pipeline processing
  if requires_aggregation_pipeline?
    # Build aggregation pipeline with $count stage
    pipeline, has_lookup_stages = build_aggregation_pipeline
    pipeline << { "$count" => "count" }

    # Auto-detect if MongoDB direct is needed. Mirror the routing in
    # #execute_aggregation_pipeline: a pipeline that references internal
    # ACL columns (_rperm/_wperm via readable_by/publicly_readable and
    # friends) MUST run mongo-direct — Parse Server's REST aggregate
    # endpoint cannot express a $match on those columns — and the
    # mongo-direct sink must be told the references are sanctioned so
    # the PipelineSecurity internal-fields denylist lets them through.
    uses_internal_fields = pipeline_uses_internal_fields?(pipeline)
    scoped = distinct_query_is_scoped?
    use_mongo_direct = false
    if defined?(@acl_query_mongo_direct) && !@acl_query_mongo_direct.nil?
      use_mongo_direct = @acl_query_mongo_direct
    elsif (scoped || has_lookup_stages || uses_internal_fields) &&
          defined?(Parse::MongoDB) && Parse::MongoDB.enabled?
      use_mongo_direct = true
    elsif scoped
      # Same fail-closed contract as #aggregate / #aggregate_from_query:
      # a scoped count must not fall back to REST /aggregate, which
      # would drop the scope and count rows the caller cannot read.
      raise_scoped_aggregation_requires_mongo_direct!
    end

    # Execute aggregation
    aggregation = Aggregation.new(self, pipeline, verbose: @verbose_aggregate,
                                  mongo_direct: use_mongo_direct,
                                  allow_internal_fields: uses_internal_fields)
    response = aggregation.execute!

    # Extract count from aggregation result
    if use_mongo_direct
      # MongoDB direct returns raw array
      return 0 if response.nil? || response.empty?
      response.first["count"] || 0
    else
      return 0 if response.error? || !response.result.is_a?(Array) || response.result.empty?
      response.result.first["count"] || 0
    end
  else
    # Use standard count endpoint for non-aggregation queries
    old_value = @count
    @count = 1
    res = client.find_objects(@table, compile.as_json, **_opts).count
    @count = old_value
    res
  end
end

#count_direct(session_token: nil, master: nil, acl_user: nil, acl_role: nil) ⇒ Integer

Note:

This is a read-only operation. Direct MongoDB queries cannot modify data.

Execute a count query directly against MongoDB, bypassing Parse Server. This is useful for performance-critical count operations.

Examples:

Basic usage

count = Song.query(:plays.gt => 1000).count_direct

With additional constraints

active_users = User.query(:status => "active").count_direct

Returns:

  • (Integer)

    the count of matching documents

Raises:

See Also:



2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
# File 'lib/parse/query.rb', line 2236

def count_direct(session_token: nil, master: nil, acl_user: nil, acl_role: nil)
  require_relative "mongodb"
  Parse::MongoDB.require_gem!

  unless Parse::MongoDB.available?
    raise Parse::MongoDB::NotEnabled,
      "Direct MongoDB queries are not enabled. " \
      "Call Parse::MongoDB.configure(uri: 'mongodb://...', enabled: true) first."
  end

  # Build the aggregation pipeline for direct MongoDB execution
  pipeline = build_direct_mongodb_pipeline

  # Remove limit and skip for count (we want total count)
  pipeline = pipeline.reject { |stage| stage.key?("$limit") || stage.key?("$skip") }

  # Add count stage
  pipeline << { "$count" => "count" }

  # When no explicit auth kwargs are provided, derive them from the
  # query's own auth state — same fallback as results_direct.
  if session_token.nil? && master.nil? && acl_user.nil? && acl_role.nil?
    auth = mongo_direct_auth_kwargs
    session_token = auth[:session_token]
    master        = auth[:master]
    acl_user      = auth[:acl_user]
    acl_role      = auth[:acl_role]
  end

  # SDK-built pipeline only — see results_direct for rationale.
  # ACL simulation runs inside Parse::MongoDB.aggregate when
  # session_token: or master: is supplied.
  raw_results = Parse::MongoDB.aggregate(@table, pipeline,
                                         allow_internal_fields: true,
                                         session_token: session_token,
                                         master: master,
                                         acl_user: acl_user,
                                         acl_role: acl_role,
                                         read_preference: @read_preference,
                                         hint: @hint)

  # Extract count from result
  return 0 if raw_results.empty?
  raw_results.first["count"] || 0
end

#count_distinct(field) ⇒ Integer

Note:

This feature requires MongoDB aggregation pipeline support in Parse Server.

Perform a count distinct query using MongoDB aggregation pipeline. This counts the number of distinct values for a given field.

Examples:

# get number of distinct genres in songs
Song.count_distinct(:genre)
# same using query instance
query = Parse::Query.new("Song")
query.where(:play_count.gt => 10)
query.count_distinct(:artist)

Parameters:

  • field (Symbol|String)

    The name of the field to count distinct values for.

Returns:

  • (Integer)

    the count of distinct values



1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
# File 'lib/parse/query.rb', line 1314

def count_distinct(field)
  if field.nil? || !field.respond_to?(:to_s)
    raise ArgumentError, "Invalid field name passed to `count_distinct`."
  end

  # Format field name according to Parse conventions
  # Handle special MongoDB field mappings for aggregation
  formatted_field = case field.to_s
    when "created_at", "createdAt"
      "_created_at"
    when "updated_at", "updatedAt"
      "_updated_at"
    else
      Query.format_field(field)
    end

  # Build the aggregation pipeline
  pipeline = [
    { "$group" => { "_id" => "$#{formatted_field}" } },
    { "$count" => "distinctCount" },
  ]

  # Use the Aggregation class to execute
  # The aggregate method will automatically handle where conditions
  aggregation = aggregate(pipeline, verbose: @verbose_aggregate)
  raw_results = aggregation.raw

  # Extract the count from the response
  if raw_results.is_a?(Array) && raw_results.first
    raw_results.first["distinctCount"] || 0
  else
    0
  end
end

#cursor(limit: 100, order: nil) ⇒ Parse::Cursor

Create a cursor-based paginator for efficiently traversing large datasets.

Cursor-based pagination is more efficient than skip/offset pagination for large datasets because it uses the last seen objectId to fetch the next page, rather than skipping over records.

Examples:

Basic usage

cursor = Song.query(:artist => "Artist").cursor(limit: 100)
cursor.each_page do |page|
  process(page)
end

Iterating over individual items

Song.query.cursor(limit: 50).each do |song|
  puts song.title
end

With custom ordering

cursor = User.query.cursor(limit: 100, order: :created_at.desc)
cursor.each_page { |page| process(page) }

Parameters:

  • limit (Integer) (defaults to: 100)

    the number of items per page (default: 100)

  • order (Parse::Order, Symbol) (defaults to: nil)

    the ordering for pagination. Defaults to :created_at.asc for stable ordering.

Returns:

See Also:



3224
3225
3226
# File 'lib/parse/query.rb', line 3224

def cursor(limit: 100, order: nil)
  Parse::Cursor.new(self, limit: limit, order: order)
end

#decode(list) ⇒ Array<Parse::Object>

Builds objects based on the set of Parse JSON hashes in an array.

Parameters:

  • list (Array<Hash>)

    a list of Parse JSON hashes

Returns:



4121
4122
4123
4124
4125
4126
4127
4128
4129
4130
# File 'lib/parse/query.rb', line 4121

def decode(list)
  # Pass fetched keys for partial fetch tracking (only if keys were specified)
  fetch_keys = @keys.present? && @keys.any? ? @keys : nil

  # Parse keys (not includes) to build nested fetched keys map
  # Keys like ["project.name", "project.status"] define which subfields to fetch on nested objects
  nested_keys = Parse::Query.parse_keys_to_nested_keys(@keys) if @keys.present?

  list.map { |m| Parse::Object.build(m, @table, fetched_keys: fetch_keys, nested_fetched_keys: nested_keys) }.compact
end

#deduplicate_consecutive_match_stages(pipeline) ⇒ Array<Hash>

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Merge consecutive $match stages in an aggregation pipeline. This optimization combines redundant stages that can occur when building pipelines from multiple constraint sources. Identical stages are deduplicated, and non-identical consecutive $match stages are merged using $and.

Parameters:

  • pipeline (Array<Hash>)

    the aggregation pipeline stages

Returns:

  • (Array<Hash>)

    the optimized pipeline with merged $match stages



3328
3329
3330
3331
3332
3333
3334
3335
3336
3337
3338
3339
3340
3341
3342
3343
3344
3345
3346
3347
3348
3349
3350
3351
3352
3353
# File 'lib/parse/query.rb', line 3328

def deduplicate_consecutive_match_stages(pipeline)
  return pipeline if pipeline.empty?

  result = []
  pipeline.each do |stage|
    if stage.is_a?(Hash) && stage.key?("$match") &&
       result.last.is_a?(Hash) && result.last.key?("$match")
      prev_match = result.last["$match"]
      curr_match = stage["$match"]

      # Skip if identical
      next if prev_match == curr_match

      # Merge the two $match stages using $and
      # Handle cases where either side might already have $and
      prev_conditions = prev_match.key?("$and") ? prev_match["$and"] : [prev_match]
      curr_conditions = curr_match.key?("$and") ? curr_match["$and"] : [curr_match]

      # Replace the previous $match with the merged version
      result[-1] = { "$match" => { "$and" => prev_conditions + curr_conditions } }
    else
      result << stage
    end
  end
  result
end

#distinct(field, return_pointers: false, mongo_direct: false, order: nil) ⇒ Object

Note:

This feature requires use of the Master Key in the API.

Queries can be made using distinct, allowing you find unique values for a specified field. For this to be performant, please remember to index your database.

Examples:

# Return a set of unique city names
# for users who are greater than 21 years old
Parse::Query.all(distinct: :age)
query = Parse::Query.new("_User")
query.where :age.gt => 21
# triggers query
query.distinct(:city) #=> ["San Diego", "Los Angeles", "San Juan"]

Parameters:

  • field (Symbol|String)

    The name of the field used for filtering.

  • mongo_direct (Boolean) (defaults to: false)

    if true, queries MongoDB directly bypassing Parse Server. Requires Parse::MongoDB to be configured. Default: false.

  • order (Symbol, nil) (defaults to: nil)

    :asc or :desc to sort the distinct values MongoDB-side via a $sort stage after $group. Default: nil (no sort — the caller can .sort the returned Array in Ruby).

Version:

  • 1.8.0



1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
# File 'lib/parse/query.rb', line 1116

def distinct(field, return_pointers: false, mongo_direct: false, order: nil)
  # Explicit opt-in to direct MongoDB
  if mongo_direct
    return distinct_direct(field, return_pointers: return_pointers, order: order,
                           **mongo_direct_auth_kwargs)
  end

  # Auto-route to mongo-direct when the compiled where contains a
  # direct-only constraint. Same gate as #count / #results.
  if requires_mongo_direct?
    assert_mongo_direct_routable!
    return distinct_direct(field, return_pointers: return_pointers, order: order,
                           **mongo_direct_auth_kwargs)
  end

  # Auto-route scoped queries (session_token / acl_user / acl_role) to
  # mongo-direct: Parse Server's REST `/aggregate` endpoint is
  # master-key-only and enforces neither ACL nor CLP, so a scoped
  # `.distinct` call against REST would silently return unscoped
  # values. The mongo-direct path runs ACLScope + CLPScope before
  # `$group`, so distinct values reflect only ACL-readable rows.
  if distinct_query_is_scoped? && defined?(Parse::MongoDB) && Parse::MongoDB.enabled?
    return distinct_direct(field, return_pointers: return_pointers, order: order,
                           **mongo_direct_auth_kwargs)
  end

  if field.nil? || !field.respond_to?(:to_s) || field.is_a?(Hash) || field.is_a?(Array)
    raise ArgumentError, "Invalid field name passed to `distinct`."
  end

  sort_dir = distinct_sort_direction(order)

  # Format field for aggregation
  formatted_field = format_aggregation_field(field)

  # Build the aggregation pipeline for distinct values
  pipeline = [{ "$group" => { "_id" => "$#{formatted_field}" } }]
  pipeline << { "$sort" => { "_id" => sort_dir } } if sort_dir
  pipeline << { "$project" => { "_id" => 0, "value" => "$_id" } }

  # Add match stage if there are where conditions
  compiled_where = compile_where
  if compiled_where.present?
    # Convert field names for aggregation context and handle dates
    aggregation_where = convert_constraints_for_aggregation(compiled_where)
    stringified_where = convert_dates_for_aggregation(aggregation_where)
    pipeline.unshift({ "$match" => stringified_where })
  end

  # Use the Aggregation class to execute
  aggregation = aggregate(pipeline, verbose: @verbose_aggregate)
  raw_results = aggregation.raw

  # Extract values from the results
  values = raw_results.map { |item| item["value"] }.compact

  # Use schema-based approach to handle pointer field results
  parse_class = Parse::Model.const_get(@table) rescue nil
  is_pointer = parse_class && is_pointer_field?(parse_class, field, formatted_field)

  if is_pointer && values.any?
    # Convert all values using schema information
    converted_values = values.map do |value|
      convert_pointer_value_with_schema(value, field, return_pointers: return_pointers)
    end
    converted_values
  elsif return_pointers
    # Explicit conversion requested - try to convert using schema or fallback to string detection
    if values.any? && values.first.is_a?(String) && values.first.include?("$")
      to_pointers(values, field)
    else
      values.map { |value| convert_pointer_value_with_schema(value, field, return_pointers: true) }
    end
  else
    # Fallback to original string detection for backward compatibility
    if values.any? && values.first.is_a?(String) && values.first.include?("$") && values.first.match(/^[A-Za-z]\w*\$\w+$/)
      first_class_name = values.first.split("$", 2)[0]
      if values.all? { |v| v.is_a?(String) && v.start_with?("#{first_class_name}$") }
        values.map { |value| value.split("$", 2)[1] }
      else
        values
      end
    else
      values
    end
  end
end

#distinct_direct(field, return_pointers: false, order: nil, session_token: nil, master: nil, acl_user: nil, acl_role: nil) ⇒ Array

Note:

This is a read-only operation. Direct MongoDB queries cannot modify data.

Execute a distinct query directly against MongoDB, bypassing Parse Server. Returns unique values for the specified field.

Examples:

Basic usage

cities = User.query(:age.gt => 21).distinct_direct(:city)
# => ["San Diego", "Los Angeles", "New York"]

With pointer fields

artists = Song.query(:plays.gt => 1000).distinct_direct(:artist, return_pointers: true)
# => [#<Parse::Pointer:Artist@abc123>, #<Parse::Pointer:Artist@def456>]

Parameters:

  • field (Symbol, String)

    the field name to get distinct values for

  • return_pointers (Boolean) (defaults to: false)

    if true, converts pointer values to Parse::Pointer objects

Returns:

  • (Array)

    array of distinct values

Raises:

See Also:



2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
# File 'lib/parse/query.rb', line 2300

def distinct_direct(field, return_pointers: false, order: nil,
                    session_token: nil, master: nil, acl_user: nil, acl_role: nil)
  require_relative "mongodb"
  Parse::MongoDB.require_gem!

  unless Parse::MongoDB.available?
    raise Parse::MongoDB::NotEnabled,
      "Direct MongoDB queries are not enabled. " \
      "Call Parse::MongoDB.configure(uri: 'mongodb://...', enabled: true) first."
  end

  if field.nil? || !field.respond_to?(:to_s) || field.is_a?(Hash) || field.is_a?(Array)
    raise ArgumentError, "Invalid field name passed to `distinct_direct`."
  end

  sort_dir = distinct_sort_direction(order)

  # Convert field name for direct MongoDB access
  mongo_field = convert_field_for_direct_mongodb(Query.format_field(field))

  # Build the base pipeline with match constraints
  pipeline = []

  # Add match stage from query constraints. `compile_where` already
  # strips `__`-prefixed routing markers, so the result is safe to
  # forward to MongoDB.
  compiled_where = compile_where
  if compiled_where.present?
    mongo_constraints = convert_constraints_for_direct_mongodb(compiled_where)
    pipeline << { "$match" => mongo_constraints } if mongo_constraints.any?
  end

  # Add group, optional sort, and project stages for distinct
  pipeline << { "$group" => { "_id" => "$#{mongo_field}" } }
  pipeline << { "$sort" => { "_id" => sort_dir } } if sort_dir
  pipeline << { "$project" => { "_id" => 0, "value" => "$_id" } }

  # SDK-built pipeline only — see results_direct for rationale.
  # Forward auth kwargs so Parse::MongoDB.aggregate runs the
  # three-layer ACL + CLP + protectedFields simulation for scoped
  # agents. Without this, distinct silently returns the unscoped
  # universe (CLP-1 enforcement asymmetry vs. #count / #results).
  # When no explicit auth kwargs are provided, derive from the
  # query's own auth state — same fallback as results_direct.
  if session_token.nil? && master.nil? && acl_user.nil? && acl_role.nil?
    auth = mongo_direct_auth_kwargs
    session_token = auth[:session_token]
    master        = auth[:master]
    acl_user      = auth[:acl_user]
    acl_role      = auth[:acl_role]
  end
  raw_results = Parse::MongoDB.aggregate(@table, pipeline,
                                         allow_internal_fields: true,
                                         read_preference: @read_preference,
                                         hint: @hint,
                                         session_token: session_token,
                                         master: master,
                                         acl_user: acl_user,
                                         acl_role: acl_role)

  # Extract values from results
  values = raw_results.map { |doc| doc["value"] }.compact

  # Handle pointer conversion if needed
  if return_pointers || field_is_pointer?(Query.format_field(field))
    values = values.map do |value|
      if value.is_a?(String) && value.include?("$")
        # MongoDB pointer format: "ClassName$objectId"
        class_name, object_id = value.split("$", 2)
        Parse::Pointer.new(class_name, object_id)
      else
        value
      end
    end
  end

  values
end

#distinct_direct_pointers(field, order: nil, session_token: nil, master: nil, acl_user: nil, acl_role: nil) ⇒ Array

Convenience method for distinct_direct that always returns Parse::Pointer objects for pointer fields.

Parameters:

Returns:

  • (Array)

    array of distinct values, with pointer fields as Parse::Pointer objects

See Also:



2384
2385
2386
2387
2388
2389
# File 'lib/parse/query.rb', line 2384

def distinct_direct_pointers(field, order: nil,
                             session_token: nil, master: nil, acl_user: nil, acl_role: nil)
  distinct_direct(field, return_pointers: true, order: order,
                  session_token: session_token, master: master,
                  acl_user: acl_user, acl_role: acl_role)
end

#distinct_objects(field, return_pointers: false) ⇒ Array

Enhanced distinct method that automatically populates Parse pointer objects at the server level. Uses aggregation pipeline to efficiently populate objects instead of post-processing.

Examples:

# Basic usage (returns raw values for non-pointer fields)
Document.query.distinct_objects(:media_format)
# => ["video", "audio", "photo"]

# Auto-populate Parse pointer objects (much faster than manual conversion)
Document.query.distinct_objects(:author_workspace)
# => [#<Workspace:0x123 @attributes={"name"=>"Workspace A", ...}>, ...]

Parameters:

  • field (Symbol, String)

    the field name to get distinct values for.

Returns:

  • (Array)

    array of distinct values, with Parse pointers populated as full objects.



4642
4643
4644
4645
4646
4647
4648
4649
# File 'lib/parse/query.rb', line 4642

def distinct_objects(field, return_pointers: false)
  if field.nil? || !field.respond_to?(:to_s)
    raise ArgumentError, "Invalid field name passed to `distinct_objects`."
  end

  # Use aggregation pipeline to get distinct values with populated objects
  execute_distinct_with_population(field, return_pointers: return_pointers)
end

#distinct_pointers(field, order: nil) ⇒ Array

Convenience method for distinct queries that always return Parse::Pointer objects for pointer fields. This is equivalent to calling distinct(field, return_pointers: true).

Parameters:

  • field (Symbol, String)

    the field name to get distinct values for

  • order (Symbol, nil) (defaults to: nil)

    forwarded to #distinct.

Returns:

  • (Array)

    array of distinct values, with pointer fields converted to Parse::Pointer objects



1209
1210
1211
# File 'lib/parse/query.rb', line 1209

def distinct_pointers(field, order: nil)
  distinct(field, return_pointers: true, order: order)
end

#distinct_query_is_scoped?Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Whether this query carries a non-master-key auth scope. Used by #distinct (and group_by aggregations) to decide whether to auto-promote the REST aggregate path to mongo-direct so the SDK's ACLScope / CLPScope enforcement actually runs.

Returns:

  • (Boolean)


1817
1818
1819
1820
1821
1822
# File 'lib/parse/query.rb', line 1817

def distinct_query_is_scoped?
  return true if @session_token.is_a?(String) && !@session_token.empty?
  return true if @acl_user
  return true if @acl_role
  false
end

#each { ... } ⇒ Array

Yields:

  • a block yield for each object in the result

Returns:

See Also:

  • Array#each


1352
1353
1354
1355
# File 'lib/parse/query.rb', line 1352

def each(&block)
  return results.enum_for(:each) unless block_given? # Sparkling magic!
  results.each(&block)
end

#exclude_keys(*fields) ⇒ self

Note:

On the REST query path (+encode: true+ in #compile) this maps to Parse Server's path-scoped +excludeKeys+. On the mongo-direct path (explicit +.results_direct+, an auto-route, or an aggregation that auto-promotes — e.g. an +$inQuery+ pointer constraint that rewrites to a +$lookup+) the pipeline can only project the #keys allowlist, so the SDK honors the denylist as a post-fetch sanitize over the returned results instead. That mongo-direct sanitize is recursive by name: it strips EVERY key with a matching name at any depth, so excluding a field also removes a same-named field inside included/nested objects — broader than the REST path's top-level/dotted scoping. Reserved envelope fields (+objectId+, +className+, +__type+, +createdAt+, +updatedAt+, +ACL+ and their Mongo storage-form names) are never stripped, so object reconstruction is unaffected. The raw aggregation accessor (aggregate(...).raw) returns unredacted documents — the sanitize applies to the object/decoded result paths. +excludeKeys+ is a projection convenience, not an ACL/CLP boundary, so it does not affect access control.

Set a server-side field denylist for this query. When set, Parse Server excludes the named fields from each returned object, complementing the #keys allowlist. The two options can be combined: Parse Server first applies the #keys allowlist, then strips any field names listed in +excludeKeys+.

Examples:

Omit a single sensitive field

Post.query.exclude_keys(:secret_token).results

Omit multiple fields

Post.query.exclude_keys(:secret_token, :internal_notes).results

Parameters:

Returns:

  • (self)


666
667
668
669
670
671
672
673
674
675
676
# File 'lib/parse/query.rb', line 666

def exclude_keys(*fields)
  @exclude_keys ||= []
  fields.flatten.each do |field|
    if field.nil? == false && field.respond_to?(:to_s)
      @exclude_keys.push Query.format_field(field).to_sym
    end
  end
  @exclude_keys.uniq!
  @results = nil if fields.count > 0
  self # chaining
end

#execute_aggregation_pipelineAggregation

Execute an aggregation pipeline for queries with pipeline constraints

Returns:

  • (Aggregation)

    the aggregation object (use .results to get Parse objects)



3801
3802
3803
3804
3805
3806
3807
3808
3809
3810
3811
3812
3813
3814
3815
3816
3817
3818
3819
3820
3821
3822
3823
3824
3825
3826
3827
3828
3829
3830
3831
3832
3833
3834
3835
3836
3837
3838
3839
3840
3841
# File 'lib/parse/query.rb', line 3801

def execute_aggregation_pipeline
  pipeline, has_lookup_stages = build_aggregation_pipeline

  # Determine if MongoDB direct should be used:
  # 1. Explicit opt-in via @acl_query_mongo_direct = true
  # 2. Auto-detect when lookup stages use $split with $literal (to parse pointer format),
  #    Parse Server's REST API can't handle it correctly
  # 3. Auto-detect when querying internal fields like _rperm or _wperm (ACL fields),
  #    Parse Server blocks these for security - must use MongoDB direct
  use_mongo_direct = false

  # When the SDK-built pipeline references internal ACL columns
  # (_rperm/_wperm via readable_by/writable_by/publicly_readable and
  # friends, or _acl), the mongo-direct sink must be told these
  # references are sanctioned so the PipelineSecurity internal-fields
  # denylist lets them through. The pipeline here is built entirely
  # from SDK constraint translation (no caller-supplied stages), so
  # this is safe — same posture as results_direct/count_direct.
  uses_internal_fields = pipeline_uses_internal_fields?(pipeline)
  scoped = distinct_query_is_scoped?

  # Check for explicit mongo_direct preference first
  if defined?(@acl_query_mongo_direct) && !@acl_query_mongo_direct.nil?
    use_mongo_direct = @acl_query_mongo_direct
  elsif defined?(Parse::MongoDB) && Parse::MongoDB.enabled?
    # Auto-detect based on pipeline contents and query scope
    if scoped || has_lookup_stages || uses_internal_fields
      use_mongo_direct = true
    end
  elsif scoped
    # Same fail-closed contract as #aggregate / #aggregate_from_query:
    # a scoped pipeline must not fall back to REST /aggregate, which
    # would drop the scope and return rows the caller cannot read.
    raise_scoped_aggregation_requires_mongo_direct!
  end

  # Create Aggregation directly to avoid double-applying constraints
  # The aggregate() method would redundantly add where constraints again
  Aggregation.new(self, pipeline, verbose: @verbose_aggregate, mongo_direct: use_mongo_direct,
                  allow_internal_fields: uses_internal_fields)
end

#explainHash

Note:

This feature requires MongoDB explain support in Parse Server. The format of the returned plan depends on the MongoDB version.

Returns the query execution plan from MongoDB. This is useful for analyzing query performance and understanding which indexes are being used.

Examples:

Get execution plan for a query

Song.query(:plays.gt => 1000).explain
# Returns detailed execution plan showing index usage, stages, etc.

Analyze a complex query

query = User.query(:email.like => "%@example.com").order(:createdAt.desc)
plan = query.explain
puts "Index used: #{plan['queryPlanner']['winningPlan']['stage']}"

Returns:

  • (Hash)

    the query execution plan from MongoDB



3299
3300
3301
3302
3303
3304
3305
3306
3307
3308
3309
3310
3311
3312
3313
3314
3315
3316
3317
3318
3319
# File 'lib/parse/query.rb', line 3299

def explain
  warn_if_public_explain_restricted!
  compiled_query = compile
  compiled_query[:explain] = true
  response = client.find_objects(@table, compiled_query.as_json, headers: _headers, **_opts)
  if response.error?
    # Parse Server 9.0+ defaults `allowPublicExplain` to false, so a
    # non-master explain that worked on 8.x now returns a permission
    # error. Surface that as actionable guidance instead of a bare 403.
    if response.respond_to?(:permission_denied?) && response.permission_denied?
      puts "[ParseQuery:Explain] #{response.error} — Parse Server 9.0+ defaults " \
           "`allowPublicExplain` to false; query explain now requires the master key " \
           "(use_master_key: true) or `allowPublicExplain: true` in the server's " \
           "databaseOptions."
    else
      puts "[ParseQuery:Explain] #{response.error}"
    end
    return {}
  end
  response.result
end

#extract_subquery_to_lookup_stages(constraints) ⇒ Hash

Extract $inQuery and $notInQuery constraints and build $lookup stages for them. This converts Parse subquery constraints into MongoDB $lookup stages that join with the related collection and filter based on the subquery conditions. Uses raw MongoDB field names (_p_field) and returns results via .raw aggregation.

Parameters:

  • constraints (Hash)

    the compiled where constraints

Returns:

  • (Hash)

    with :constraints (remaining), :lookup_stages, and :post_lookup_match



3971
3972
3973
3974
3975
3976
3977
3978
3979
3980
3981
3982
3983
3984
3985
3986
3987
3988
3989
3990
3991
3992
3993
3994
3995
3996
3997
3998
3999
4000
4001
4002
4003
4004
4005
4006
4007
4008
4009
4010
4011
4012
4013
4014
4015
4016
4017
4018
4019
4020
4021
4022
4023
4024
4025
4026
4027
4028
4029
4030
4031
4032
4033
4034
4035
4036
4037
4038
4039
4040
4041
4042
4043
4044
4045
4046
4047
4048
4049
4050
4051
4052
4053
4054
4055
4056
4057
4058
4059
# File 'lib/parse/query.rb', line 3971

def extract_subquery_to_lookup_stages(constraints)
  return { constraints: constraints, lookup_stages: [], post_lookup_match: {} } unless constraints.is_a?(Hash)

  remaining_constraints = {}
  lookup_stages = []
  post_lookup_match = {}

  constraints.each do |field, value|
    # Check for both string and symbol keys
    has_in_query = value.is_a?(Hash) && (value.key?("$inQuery") || value.key?(:"$inQuery"))
    has_not_in_query = value.is_a?(Hash) && (value.key?("$notInQuery") || value.key?(:"$notInQuery"))

    if has_in_query || has_not_in_query
      is_in_query = has_in_query
      # Get the subquery config using the correct key type
      in_query_key = value.key?("$inQuery") ? "$inQuery" : :"$inQuery"
      not_in_query_key = value.key?("$notInQuery") ? "$notInQuery" : :"$notInQuery"
      subquery_config = value[is_in_query ? in_query_key : not_in_query_key]
      # Handle both string and symbol keys in the subquery config
      class_name = subquery_config["className"] || subquery_config[:className]
      where_clause = subquery_config["where"] || subquery_config[:where] || {}

      # Format field name for the pointer
      formatted_field = Query.format_field(field)
      mongo_pointer_field = "_p_#{formatted_field}"
      lookup_result_field = "_lookup_#{formatted_field}_result"
      lookup_id_field = "_lookup_#{formatted_field}_id"

      # Stage 1: Extract objectId from the pointer field using $split
      # Parse Server stores pointers as _p_fieldName with format "ClassName$objectId"
      # Use $literal to escape the $ character in the delimiter
      lookup_stages << {
        "$addFields" => {
          lookup_id_field => {
            "$arrayElemAt" => [
              { "$split" => ["$#{mongo_pointer_field}", { "$literal" => "$" }] },
              1,
            ],
          },
        },
      }

      # Stage 2: $lookup to join with the related collection
      # Build pipeline to match on _id and apply where conditions
      lookup_pipeline = [
        { "$match" => { "$expr" => { "$eq" => ["$_id", "$$lookupId"] } } },
      ]

      # Add where conditions to lookup pipeline if present
      if where_clause.any?
        converted_where = convert_dates_for_aggregation(where_clause)
        converted_where = convert_constraints_for_aggregation(converted_where)
        lookup_pipeline << { "$match" => converted_where }
      end

      lookup_stages << {
        "$lookup" => {
          "from" => class_name,
          "let" => { "lookupId" => "$#{lookup_id_field}" },
          "pipeline" => lookup_pipeline,
          "as" => lookup_result_field,
        },
      }

      # Match based on whether lookup returned results
      if is_in_query
        # $inQuery: keep documents where lookup found matches
        post_lookup_match[lookup_result_field] = { "$ne" => [] }
      else
        # $notInQuery: keep documents where lookup found no matches
        post_lookup_match[lookup_result_field] = { "$eq" => [] }
      end
    elsif value.is_a?(Hash)
      # Recursively handle nested constraints
      nested = extract_subquery_to_lookup_stages(value)
      if nested[:lookup_stages].any?
        lookup_stages.concat(nested[:lookup_stages])
        post_lookup_match.merge!(nested[:post_lookup_match])
        remaining_constraints[field] = nested[:constraints]
      else
        remaining_constraints[field] = value
      end
    else
      remaining_constraints[field] = value
    end
  end

  { constraints: remaining_constraints, lookup_stages: lookup_stages, post_lookup_match: post_lookup_match }
end

#fetch!(compiled_query) ⇒ Parse::Response Also known as: execute!

Performs the fetch request for the query.

Parameters:

  • compiled_query (Hash)

    the compiled query

Returns:



1691
1692
1693
1694
1695
1696
1697
# File 'lib/parse/query.rb', line 1691

def fetch!(compiled_query)
  response = client.find_objects(@table, compiled_query.as_json, headers: _headers, **_opts)
  if response.error?
    puts "[ParseQuery] #{response.error}"
  end
  response
end

#first(limit = 1) ⇒ Parse::Object #first(constraints = {}) ⇒ Parse::Object

Note:

Supports all constraint options like :keys, :includes, :order, etc.

Overloads:

  • #first(limit = 1) ⇒ Parse::Object

    Returns the first object from the result.

    Parameters:

    • limit (Integer) (defaults to: 1)

      the number of first items to return.

    Returns:

  • #first(constraints = {}) ⇒ Parse::Object

    Returns the first object from the result.

    Parameters:

    • constraints (Hash) (defaults to: {})

      query constraints to apply before fetching.

    Returns:

Parameters:

  • mongo_direct (Boolean) (defaults to: false)

    if true, queries MongoDB directly bypassing Parse Server. Requires Parse::MongoDB to be configured. Default: false.



1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
# File 'lib/parse/query.rb', line 1388

def first(limit_or_constraints = 1, mongo_direct: false, **options)
  # Use direct MongoDB query if requested
  if mongo_direct
    return first_direct(limit_or_constraints)
  end

  fetch_count = 1
  if limit_or_constraints.is_a?(Hash)
    conditions(limit_or_constraints)
    # Check if limit was set in constraints, otherwise use 1
    # Handle :max case - if @limit is :max, default to 1 for first()
    fetch_count = (@limit.is_a?(Numeric) ? @limit : nil) || 1
    # Set @limit to ensure query only fetches the needed records
    @results = nil if @limit != fetch_count
    @limit = fetch_count
  else
    fetch_count =
      case limit_or_constraints
      when Numeric then limit_or_constraints.to_i
      when String
        unless limit_or_constraints =~ /\A-?\d+\z/
          raise ArgumentError,
                "Invalid first() argument #{limit_or_constraints.inspect}. " \
                "Expected an Integer, a numeric String, or a Hash of constraints."
        end
        limit_or_constraints.to_i
      else
        raise ArgumentError,
              "Invalid first() argument #{limit_or_constraints.inspect}. " \
              "Expected an Integer, a numeric String, or a Hash of constraints."
      end
    @results = nil if @limit != fetch_count
    @limit = fetch_count
  end
  # Apply any additional keyword options as conditions (e.g., keys:, includes:)
  conditions(options) unless options.empty?
  fetch_count == 1 ? results.first : results.first(fetch_count)
end

#first_direct(limit_or_constraints = 1) ⇒ Parse::Object, ...

Note:

This is a read-only operation. Direct MongoDB queries cannot modify data.

Execute the query directly against MongoDB and return the first result. This is useful for performance-critical single-object lookups.

Examples:

Basic usage

song = Song.query(:objectId => "abc123").first_direct

With limit

top_songs = Song.query(:plays.gt => 1000).order(:plays.desc).first_direct(5)

Parameters:

  • limit_or_constraints (Integer, Hash) (defaults to: 1)

    either the number of results to return, or a hash of additional constraints to apply

Returns:

Raises:

See Also:



2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
# File 'lib/parse/query.rb', line 2186

def first_direct(limit_or_constraints = 1)
  if limit_or_constraints.is_a?(Hash)
    conditions(limit_or_constraints)
    limit_or_constraints = 1
  end

  count =
    case limit_or_constraints
    when Numeric then limit_or_constraints.to_i
    when String
      unless limit_or_constraints =~ /\A-?\d+\z/
        raise ArgumentError,
              "Invalid first_direct() argument #{limit_or_constraints.inspect}. " \
              "Expected an Integer, a numeric String, or a Hash of constraints."
      end
      limit_or_constraints.to_i
    else
      raise ArgumentError,
            "Invalid first_direct() argument #{limit_or_constraints.inspect}. " \
            "Expected an Integer, a numeric String, or a Hash of constraints."
    end
  count = 1 if count <= 0

  # Set limit for single/few results
  original_limit = @limit
  @limit = count

  begin
    items = results_direct
  ensure
    @limit = original_limit
  end

  count == 1 ? items.first : items.first(count)
end

#get(object_id) ⇒ Parse::Object

Retrieve a single object by its objectId.

Parameters:

  • object_id (String)

    the objectId to retrieve.

Returns:

Raises:



1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
# File 'lib/parse/query.rb', line 1475

def get(object_id)
  parse_class = Object.const_get(@table) if Object.const_defined?(@table)
  parse_class ||= Parse::Object

  response = client.fetch_object(@table, object_id)
  if response.error?
    raise Parse::Error.new(response.code, response.error)
  end

  Parse::Object.build(response.result, parse_class)
end

#get_pointer_target_class(field) ⇒ String?

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Get the target class name for a pointer field from model references. Uses the model's references hash which maps field names to target class names.

Parameters:

  • field (Symbol)

    the field name

Returns:

  • (String, nil)

    the target class name or nil if not found



2793
2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
# File 'lib/parse/query.rb', line 2793

def get_pointer_target_class(field)
  begin
    klass = Parse::Model.find_class(@table)
    return nil unless klass.respond_to?(:references)

    references = klass.references
    return nil if references.nil? || references.empty?

    # Check both the field name and its formatted Parse field name
    formatted_field = Query.format_field(field).to_sym

    # Try direct lookup first, then formatted field
    target = references[field] || references[formatted_field]

    # Also check field_map for aliased fields
    if target.nil? && klass.respond_to?(:field_map)
      mapped_field = klass.field_map[field]
      target = references[mapped_field] if mapped_field
    end

    target
  rescue NameError, StandardError
    nil
  end
end

#group_by(field, flatten_arrays: false, sortable: false, return_pointers: false, mongo_direct: false) ⇒ GroupBy, SortableGroupBy

Group results by a specific field and return a GroupBy object for chaining aggregations.

Examples:

Document.group_by(:category).count
Document.where(:status => "active").group_by(:project).sum(:file_size)
Document.group_by(:media_format).average(:duration)

# Array flattening example:
# Record 1: tags = ["a", "b"]
# Record 2: tags = ["b", "c"]
Document.group_by(:tags, flatten_arrays: true).count
# => {"a" => 1, "b" => 2, "c" => 1}

# Sortable results:
Document.group_by(:category, sortable: true).count.sort_by_value_desc
# => [["video", 45], ["image", 23], ["audio", 12]]

# Return Parse::Pointer objects for pointer fields:
Document.group_by(:author_workspace, return_pointers: true).count
# => {#<Parse::Pointer @parse_class="Workspace" @id="team1"> => 5, ...}

Parameters:

  • field (Symbol, String)

    the field name to group by.

  • flatten_arrays (Boolean) (defaults to: false)

    if true, arrays will be flattened before grouping. This allows counting/aggregating individual array elements across all records.

  • sortable (Boolean) (defaults to: false)

    if true, returns a SortableGroupBy that supports sorting results.

  • return_pointers (Boolean) (defaults to: false)

    if true, converts Parse pointer group keys to Parse::Pointer objects.

  • mongo_direct (Boolean) (defaults to: false)

    if true, queries MongoDB directly bypassing Parse Server. Requires Parse::MongoDB to be configured. Default: false.

Returns:



4454
4455
4456
4457
4458
4459
4460
4461
4462
4463
4464
# File 'lib/parse/query.rb', line 4454

def group_by(field, flatten_arrays: false, sortable: false, return_pointers: false, mongo_direct: false)
  if field.nil? || !field.respond_to?(:to_s)
    raise ArgumentError, "Invalid field name passed to `group_by`."
  end

  if sortable
    SortableGroupBy.new(self, field, flatten_arrays: flatten_arrays, return_pointers: return_pointers, mongo_direct: mongo_direct)
  else
    GroupBy.new(self, field, flatten_arrays: flatten_arrays, return_pointers: return_pointers, mongo_direct: mongo_direct)
  end
end

#group_by_date(field, interval, sortable: false, return_pointers: false, timezone: nil, mongo_direct: false) ⇒ GroupByDate, SortableGroupByDate

Group results by a date field at specified time intervals.

Examples:

Post.group_by_date(:created_at, :day).count
Document.group_by_date(:created_at, :month).sum(:file_size)
Post.where(:project => project_id).group_by_date(:created_at, :week).average(:duration)

# Sortable date results:
Document.group_by_date(:created_at, :day, sortable: true).count.sort_by_value_desc
# => [["2024-11-25", 45], ["2024-11-24", 23], ...]

Parameters:

  • field (Symbol, String)

    the date field name to group by.

  • interval (Symbol)

    the time interval (:year, :month, :week, :day, :hour).

  • sortable (Boolean) (defaults to: false)

    if true, returns a SortableGroupByDate that supports sorting results.

  • return_pointers (Boolean) (defaults to: false)

    if true, converts Parse pointer values to Parse::Pointer objects. Note: This is primarily for consistency - date groupings typically use formatted date strings as keys.

  • mongo_direct (Boolean) (defaults to: false)

    if true, queries MongoDB directly bypassing Parse Server. Requires Parse::MongoDB to be configured. Default: false.

Returns:



4614
4615
4616
4617
4618
4619
4620
4621
4622
4623
4624
4625
4626
4627
4628
# File 'lib/parse/query.rb', line 4614

def group_by_date(field, interval, sortable: false, return_pointers: false, timezone: nil, mongo_direct: false)
  if field.nil? || !field.respond_to?(:to_s)
    raise ArgumentError, "Invalid field name passed to `group_by_date`."
  end

  unless [:year, :month, :week, :day, :hour, :minute, :second].include?(interval.to_sym)
    raise ArgumentError, "Invalid interval. Must be one of: :year, :month, :week, :day, :hour, :minute, :second"
  end

  if sortable
    SortableGroupByDate.new(self, field, interval.to_sym, return_pointers: return_pointers, timezone: timezone, mongo_direct: mongo_direct)
  else
    GroupByDate.new(self, field, interval.to_sym, return_pointers: return_pointers, timezone: timezone, mongo_direct: mongo_direct)
  end
end

#group_objects_by(field, return_pointers: false) ⇒ Hash

Group Parse objects by a field value and return arrays of actual objects. Unlike group_by which uses aggregation for counts/sums, this fetches all objects and groups them in Ruby, returning the actual Parse object instances.

Examples:

# Get arrays of actual Document objects grouped by category
Document.query.group_objects_by(:category)
# => {
#   "video" => [#<Document:video1>, #<Document:video2>, ...],
#   "image" => [#<Document:image1>, #<Document:image2>, ...],
#   "audio" => [#<Document:audio1>, ...]
# }

# Get Parse::Pointer objects instead (memory efficient)
Document.query.group_objects_by(:category, return_pointers: true)
# => {
#   "video" => [#<Parse::Pointer>, #<Parse::Pointer>, ...],
#   "image" => [#<Parse::Pointer>, ...],
#   "audio" => [#<Parse::Pointer>, ...]
# }

Parameters:

  • field (Symbol, String)

    the field name to group by.

  • return_pointers (Boolean) (defaults to: false)

    if true, returns Parse::Pointer objects instead of full objects.

Returns:

  • (Hash)

    a hash with field values as keys and arrays of Parse objects as values.



4488
4489
4490
4491
4492
4493
4494
4495
4496
4497
4498
4499
4500
4501
4502
4503
4504
4505
4506
4507
4508
4509
4510
4511
4512
4513
4514
4515
4516
4517
4518
4519
4520
4521
4522
4523
4524
4525
4526
4527
4528
4529
4530
# File 'lib/parse/query.rb', line 4488

def group_objects_by(field, return_pointers: false)
  if field.nil? || !field.respond_to?(:to_s)
    raise ArgumentError, "Invalid field name passed to `group_objects_by`."
  end

  # Fetch all objects that match the query
  objects = results(return_pointers: return_pointers)

  # Group objects by the specified field value
  grouped = {}
  objects.each do |obj|
    # Get the field value for grouping
    field_value = if obj.respond_to?(:attributes)
        # For Parse objects, try multiple field access patterns
        obj.attributes[field.to_s] ||
        obj.attributes[Query.format_field(field).to_s] ||
        (obj.respond_to?(field) ? obj.send(field) : nil)
      elsif obj.is_a?(Hash)
        # For raw JSON objects, try multiple field access patterns
        obj[field.to_s] ||
        obj[Query.format_field(field).to_s] ||
        obj[field.to_sym] ||
        obj[Query.format_field(field).to_sym]
      else
        # Fallback - try to access as method
        obj.respond_to?(field) ? obj.send(field) : nil
      end

    # Handle nil field values
    group_key = field_value.nil? ? "null" : field_value

    # Convert Parse pointer values to readable format for grouping key
    if group_key.is_a?(Hash) && group_key["__type"] == "Pointer"
      group_key = "#{group_key["className"]}##{group_key["objectId"]}"
    end

    # Initialize array if this is the first object for this group
    grouped[group_key] ||= []
    grouped[group_key] << obj
  end

  grouped
end

#has_subquery_constraints?(constraints) ⇒ Boolean

Check if constraints contain $inQuery or $notInQuery that need resolution

Parameters:

  • constraints (Hash)

    the compiled where constraints

Returns:

  • (Boolean)

    true if subquery constraints are present



4087
4088
4089
4090
4091
4092
4093
4094
4095
4096
4097
4098
4099
4100
4101
# File 'lib/parse/query.rb', line 4087

def has_subquery_constraints?(constraints)
  return false unless constraints.is_a?(Hash)

  constraints.any? do |field, value|
    if value.is_a?(Hash)
      # Check for both string and symbol keys since constraints can come from
      # different sources (JSON parsing vs Ruby symbol keys)
      value.key?("$inQuery") || value.key?(:"$inQuery") ||
      value.key?("$notInQuery") || value.key?(:"$notInQuery") ||
      has_subquery_constraints?(value)
    else
      false
    end
  end
end

#include(*fields) ⇒ Object

alias for includes



911
912
913
# File 'lib/parse/query.rb', line 911

def include(*fields)
  includes(*fields)
end

#includes(*fields) ⇒ self

Set a list of Parse Pointer columns to be fetched for matching records. You may chain multiple columns with the . operator.

Examples:

# assuming an 'Artist' has a pointer column for a 'Manager'
# and a Song has a pointer column for an 'Artist'.

# include the full artist object
Song.all(:includes => [:artist])

# Chaining - fetches the artist and the artist's manager for matching songs
Song.all :includes => ['artist.manager']

Parameters:

  • fields (Array)

    the list of Pointer columns to fetch.

Returns:

  • (self)


898
899
900
901
902
903
904
905
906
907
908
# File 'lib/parse/query.rb', line 898

def includes(*fields)
  @includes ||= []
  fields.flatten.each do |field|
    if field.nil? == false && field.respond_to?(:to_s)
      @includes.push Query.format_field(field).to_sym
    end
  end
  @includes.uniq!
  @results = nil if fields.count > 0
  self # chaining
end

#keys(*fields) ⇒ self Also known as: select_fields

Note:

Use this feature with caution when working with the results, as values for the fields not specified in the query will be omitted in the resulting object.

Restrict the fields returned by the query. This is useful for larger query results set where some of the data will not be used, which reduces network traffic and deserialization performance.

Examples:

# results only contain :name field
Song.all :keys => :name

# multiple keys
Song.all :keys => [:name,:artist]

Parameters:

  • fields (Array)

    the name of the fields to return.

Returns:

  • (self)


620
621
622
623
624
625
626
627
628
629
630
# File 'lib/parse/query.rb', line 620

def keys(*fields)
  @keys ||= []
  fields.flatten.each do |field|
    if field.nil? == false && field.respond_to?(:to_s)
      @keys.push Query.format_field(field).to_sym
    end
  end
  @keys.uniq!
  @results = nil if fields.count > 0
  self # chaining
end

#last_updated(limit = 1, **options) ⇒ Parse::Object+

Note:

Supports all constraint options like :keys, :includes, :limit, etc.

Returns the most recently updated object(s) (ordered by updated_at descending).

Examples:

query.last_updated                          # single most recently updated
query.last_updated(5)                       # 5 most recently updated
query.last_updated(:user.eq => x)           # most recently updated for user
query.last_updated(:user.eq => x, limit: 5) # 5 most recently updated for user

Parameters:

  • limit (Integer) (defaults to: 1)

    the number of items to return (default: 1).

Returns:



1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
# File 'lib/parse/query.rb', line 1459

def last_updated(limit = 1, **options)
  # Allow limit to be overridden via options
  limit = options.delete(:limit) if options.key?(:limit)
  @results = nil if @limit != limit
  @limit = limit
  # Add updated_at descending order if not already present
  order(:updated_at.desc) unless @order.any? { |o| o.operand == :updated_at }
  # Apply any additional keyword options as conditions (e.g., keys:, includes:)
  conditions(options) unless options.empty?
  limit == 1 ? results.first : results.first(limit)
end

#latest(limit = 1, **options) ⇒ Parse::Object+

Note:

Supports all constraint options like :keys, :includes, :limit, etc.

Returns the most recently created object(s) (ordered by created_at descending).

Examples:

query.latest                          # single most recent
query.latest(5)                       # 5 most recent
query.latest(:user.eq => x)           # most recent for user
query.latest(:user.eq => x, limit: 5) # 5 most recent for user

Parameters:

  • limit (Integer) (defaults to: 1)

    the number of items to return (default: 1).

Returns:



1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
# File 'lib/parse/query.rb', line 1437

def latest(limit = 1, **options)
  # Allow limit to be overridden via options
  limit = options.delete(:limit) if options.key?(:limit)
  @results = nil if @limit != limit
  @limit = limit
  # Add created_at descending order if not already present
  order(:created_at.desc) unless @order.any? { |o| o.operand == :created_at }
  # Apply any additional keyword options as conditions (e.g., keys:, includes:)
  conditions(options) unless options.empty?
  limit == 1 ? results.first : results.first(limit)
end

#limit(count) ⇒ self

Limit the number of objects returned by the query. The default is 100, with Parse allowing a maximum of 1000. The framework also allows a value of :max. Utilizing this will have the framework continually intelligently utilize :skip to continue to paginate through results until no more results match the query criteria. When utilizing all(), :max is the default option for :limit.

Examples:

Song.all :limit => 1 # same as Song.first
Song.all :limit => 2025 # large limits supported.
Song.all :limit => :max # as many records as possible.

Parameters:

  • count (Integer, Symbol, String, nil)

    The number of records to return. Pass +:max+ to fetch as many records as possible (Parse-Server dependent). Numeric strings (e.g. +"50"+) are coerced to Integer. Pass +nil+ to explicitly clear the limit. Any other value raises +ArgumentError+ rather than silently disabling the limit.

Returns:

  • (self)


816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
# File 'lib/parse/query.rb', line 816

def limit(count)
  case count
  when nil
    @limit = nil
  when Numeric
    @limit = [0, count.to_i].max
  when :max
    @limit = :max
  when String
    unless count =~ /\A-?\d+\z/
      raise ArgumentError,
            "Invalid limit #{count.inspect}. Expected an Integer, :max, " \
            "a numeric String, or nil."
    end
    @limit = [0, count.to_i].max
  else
    raise ArgumentError,
          "Invalid limit #{count.inspect}. Expected an Integer, :max, " \
          "a numeric String, or nil."
  end

  @results = nil
  self #chaining
end

#map { ... } ⇒ Array

Yields:

  • a block yield for each object in the result

Returns:

See Also:

  • Array#map


1360
1361
1362
1363
# File 'lib/parse/query.rb', line 1360

def map(&block)
  return results.enum_for(:map) unless block_given? # Sparkling magic!
  results.map(&block)
end

#max(field) ⇒ Object

Find the maximum value for a specific field.

Parameters:

  • field (Symbol, String)

    the field name to find maximum for.

Returns:

  • (Object)

    the maximum value for the field, or nil if no results.



4411
4412
4413
4414
4415
4416
4417
4418
4419
4420
4421
4422
4423
4424
4425
# File 'lib/parse/query.rb', line 4411

def max(field)
  if field.nil? || !field.respond_to?(:to_s)
    raise ArgumentError, "Invalid field name passed to `max`."
  end

  # Format field name according to Parse conventions
  formatted_field = format_aggregation_field(field)

  # Build the aggregation pipeline
  pipeline = [
    { "$group" => { "_id" => nil, "max" => { "$max" => "$#{formatted_field}" } } },
  ]

  execute_basic_aggregation(pipeline, "max", field, "max")
end

#min(field) ⇒ Object

Find the minimum value for a specific field.

Parameters:

  • field (Symbol, String)

    the field name to find minimum for.

Returns:

  • (Object)

    the minimum value for the field, or nil if no results.



4392
4393
4394
4395
4396
4397
4398
4399
4400
4401
4402
4403
4404
4405
4406
# File 'lib/parse/query.rb', line 4392

def min(field)
  if field.nil? || !field.respond_to?(:to_s)
    raise ArgumentError, "Invalid field name passed to `min`."
  end

  # Format field name according to Parse conventions
  formatted_field = format_aggregation_field(field)

  # Build the aggregation pipeline
  pipeline = [
    { "$group" => { "_id" => nil, "min" => { "$min" => "$#{formatted_field}" } } },
  ]

  execute_basic_aggregation(pipeline, "min", field, "min")
end

#not_publicly_readable(mongo_direct: nil) ⇒ Parse::Query

Find objects that are NOT publicly readable. Matches objects where _rperm does NOT contain "*".

Examples:

Song.query.not_publicly_readable.results

Parameters:

  • mongo_direct (Boolean) (defaults to: nil)

    if true, forces MongoDB direct query.

Returns:



5787
5788
5789
5790
5791
# File 'lib/parse/query.rb', line 5787

def not_publicly_readable(mongo_direct: nil)
  @acl_query_mongo_direct = mongo_direct unless mongo_direct.nil?
  where(:ACL.not_readable_by => "*")
  self
end

#not_publicly_writable(mongo_direct: nil) ⇒ Parse::Query

Find objects that are NOT publicly writable. Matches objects where _wperm does NOT contain "*".

Examples:

Song.query.not_publicly_writable.results

Parameters:

  • mongo_direct (Boolean) (defaults to: nil)

    if true, forces MongoDB direct query.

Returns:



5800
5801
5802
5803
5804
# File 'lib/parse/query.rb', line 5800

def not_publicly_writable(mongo_direct: nil)
  @acl_query_mongo_direct = mongo_direct unless mongo_direct.nil?
  where(:ACL.not_writable_by => "*")
  self
end

#not_readable_by(permission, mongo_direct: nil) ⇒ Parse::Query

Find objects that are NOT readable by the given principal — i.e. hidden from them. Excludes rows readable by the principal directly, via any role they inherit, OR publicly (a public row is readable by everyone), and excludes rows with a missing _rperm (public by absence).

Examples:

Song.query.not_readable_by(current_user).results   # hidden from this user

Parameters:

Returns:



5759
5760
5761
5762
5763
# File 'lib/parse/query.rb', line 5759

def not_readable_by(permission, mongo_direct: nil)
  @acl_query_mongo_direct = mongo_direct unless mongo_direct.nil?
  where(:ACL.not_readable_by => permission)
  self
end

#not_writable_by(permission, mongo_direct: nil) ⇒ Parse::Query

Find objects that are NOT writable by the given principal. See #not_readable_by for the exclusion semantics (direct, role, public).

Examples:

Song.query.not_writable_by("role:Admin").results

Parameters:

Returns:



5774
5775
5776
5777
5778
# File 'lib/parse/query.rb', line 5774

def not_writable_by(permission, mongo_direct: nil)
  @acl_query_mongo_direct = mongo_direct unless mongo_direct.nil?
  where(:ACL.not_writable_by => permission)
  self
end

#or_where(where_clauses = []) ⇒ Query

Combine two where clauses into an OR constraint. Equivalent to the $or Parse query operation. This is useful if you want to find objects that match several queries. We overload the | operator in order to have a clean syntax for joining these or operations.

Examples:

query = Player.where(:wins.gt => 150)
query.or_where(:wins.lt => 5)
# where wins > 150 || wins < 5
results = query.results

# or_query = query1 | query2 | query3 ...
# ex. where wins > 150 || wins < 5
query = Player.where(:wins.gt => 150) | Player.where(:wins.lt => 5)
results = query.results

Parameters:

  • where_clauses (Array<Parse::Constraint>) (defaults to: [])

    a list of Parse::Constraint objects to combine.

Returns:

  • (Query)

    the combined query with an OR clause.



1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
# File 'lib/parse/query.rb', line 1061

def or_where(where_clauses = [])
  where_clauses = where_clauses.where if where_clauses.is_a?(Parse::Query)
  where_clauses = Parse::Query.new(@table, where_clauses).where if where_clauses.is_a?(Hash)
  return self if where_clauses.blank?
  # we can only have one compound query constraint. If we need to add another OR clause
  # let's find the one we have (if any)
  compound = @where.find { |f| f.is_a?(Parse::Constraint::CompoundQueryConstraint) }
  # create a set of clauses that are not an OR clause.
  remaining_clauses = @where.select { |f| f.is_a?(Parse::Constraint::CompoundQueryConstraint) == false }
  # if we don't have a OR clause to reuse, then create a new one with then
  # current set of constraints
  if compound.blank?
    initial_constraints = Parse::Query.compile_where(remaining_clauses)
    # Only include initial constraints if they're not empty
    initial_values = initial_constraints.empty? ? [] : [initial_constraints]
    compound = Parse::Constraint::CompoundQueryConstraint.new :or, initial_values
  end
  # then take the where clauses from the second query and append them.
  new_constraints = Parse::Query.compile_where(where_clauses)
  # Only add new constraints if they're not empty
  unless new_constraints.empty?
    compound.value.push new_constraints
  end
  #compound = Parse::Constraint::CompoundQueryConstraint.new :or, [remaining_clauses, or_where_query.where]
  @where = [compound]
  self #chaining
end

#order(*ordering) ⇒ self

Add a sorting order for the query.

Examples:

# order updated_at ascending order
Song.all :order => :updated_at

# first order by highest like_count, then by ascending name.
# Note that ascending is the default if not specified (ex. `:name.asc`)
Song.all :order => [:like_count.desc, :name]

# hash form: {field => :asc | :desc | "asc" | "desc"}
Song.all :order => { :like_count => :desc, :name => :asc }

Parameters:

  • ordering (Parse::Order, Symbol, String, Hash)

    one or more ordering directives. A Hash maps field => direction. Unsupported argument types raise +ArgumentError+ rather than being silently dropped.

Returns:

  • (self)


737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
# File 'lib/parse/query.rb', line 737

def order(*ordering)
  @order ||= []
  # Don't flatten through Hashes — flatten only unpacks Arrays.
  ordering.flatten.each do |entry|
    case entry
    when Order
      entry.field = Query.format_field(entry.field)
      @order.push entry
    when Symbol, String
      o = Order.new(entry)
      o.field = Query.format_field(o.field)
      @order.push o
    when Hash
      entry.each do |field, direction|
        dir_sym = direction.is_a?(String) ? direction.downcase.to_sym : direction
        unless dir_sym == :asc || dir_sym == :desc
          raise ArgumentError,
                "Invalid order direction #{direction.inspect} for field " \
                "#{field.inspect}. Expected :asc or :desc."
        end
        o = Order.new(field, dir_sym)
        o.field = Query.format_field(o.field)
        @order.push o
      end
    else
      raise ArgumentError,
            "Invalid order argument #{entry.inspect}. Expected a Symbol, " \
            "String, Parse::Order (e.g. :field.asc / :field.desc), or " \
            "Hash of {field => :asc | :desc}."
    end
  end
  @results = nil if ordering.count > 0
  self #chaining
end

#pipelineArray

Returns the aggregation pipeline for this query if it contains pipeline-based constraints

Returns:

  • (Array)

    the aggregation pipeline stages, or empty array if no pipeline needed



4321
4322
4323
4324
4325
4326
4327
4328
4329
4330
4331
4332
4333
4334
4335
# File 'lib/parse/query.rb', line 4321

def pipeline
  pipeline_stages = []

  # Check if any constraints generate aggregation pipelines
  @where.each do |constraint|
    if constraint.respond_to?(:as_json)
      constraint_json = constraint.as_json
      if constraint_json.is_a?(Hash) && constraint_json.has_key?("__aggregation_pipeline")
        pipeline_stages.concat(constraint_json["__aggregation_pipeline"])
      end
    end
  end

  pipeline_stages
end

#pipeline_uses_internal_fields?(pipeline) ⇒ Boolean

Check if the pipeline references internal Parse fields that require MongoDB direct access

Parameters:

  • pipeline (Array)

    the aggregation pipeline stages

Returns:

  • (Boolean)

    true if internal fields are used



3846
3847
3848
3849
3850
# File 'lib/parse/query.rb', line 3846

def pipeline_uses_internal_fields?(pipeline)
  internal_fields = %w[_rperm _wperm _acl]
  pipeline_json = pipeline.to_json
  internal_fields.any? { |field| pipeline_json.include?(field) }
end

#pluck(field) ⇒ Array

Extract values for a specific field from all matching objects. This is similar to keys() but returns an array of the actual field values instead of objects with only those fields selected.

Examples:

# Get all asset names
Document.query.pluck(:name)
# => ["video1.mp4", "image1.jpg", "audio1.mp3"]

# Get all author workspace IDs
Document.query.pluck(:author_workspace)
# => [{"__type"=>"Pointer", "className"=>"Workspace", "objectId"=>"abc123"}, ...]

# Get created dates
Document.query.pluck(:created_at)
# => [2024-11-24 10:30:00 UTC, 2024-11-25 14:20:00 UTC, ...]

Parameters:

  • field (Symbol, String)

    the field name to extract values for.

Returns:

  • (Array)

    an array of field values from all matching objects.



695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
# File 'lib/parse/query.rb', line 695

def pluck(field)
  if field.nil? || !field.respond_to?(:to_s)
    raise ArgumentError, "Invalid field name passed to `pluck`."
  end

  # Use keys to select only the field we want for efficiency
  query_with_field = self.dup.keys(field)

  # Get the results and extract the field values
  objects = query_with_field.results
  formatted_field = Query.format_field(field)

  objects.map do |obj|
    if obj.respond_to?(:attributes)
      # For Parse objects, get the attribute value
      obj.attributes[field.to_s] || obj.attributes[formatted_field.to_s]
    elsif obj.is_a?(Hash)
      # For raw JSON objects
      obj[field.to_s] || obj[formatted_field.to_s]
    else
      # Fallback - try to access as method
      obj.respond_to?(field) ? obj.send(field) : nil
    end
  end
end

#prepared(includeClassName: false) ⇒ Hash

Returns a compiled query without encoding the where clause.

Parameters:

  • includeClassName (Boolean) (defaults to: false)

    whether to include the class name of the collection in the resulting compiled query.

Returns:

  • (Hash)

    a hash representing the prepared query request.



4246
4247
4248
# File 'lib/parse/query.rb', line 4246

def prepared(includeClassName: false)
  compile(encode: false, includeClassName: includeClassName)
end

#prettyString

Retruns a formatted JSON string representing the query, useful for debugging.

Returns:



4345
4346
4347
# File 'lib/parse/query.rb', line 4345

def pretty
  JSON.pretty_generate(as_json)
end

#private_acl(mongo_direct: nil) ⇒ Parse::Query Also known as: master_key_only

Find objects with completely private ACL (no read AND no write permissions). Only accessible with master key.

Examples:

Song.query.private_acl.results
Song.query.master_key_only.results  # Alias

Parameters:

  • mongo_direct (Boolean) (defaults to: nil)

    if true, forces MongoDB direct query.

Returns:



5741
5742
5743
5744
# File 'lib/parse/query.rb', line 5741

def private_acl(mongo_direct: nil)
  privately_readable(mongo_direct: mongo_direct)
  privately_writable(mongo_direct: mongo_direct)
end

#privately_readable(mongo_direct: nil) ⇒ Parse::Query Also known as: master_key_read_only

Find objects with no read permissions (master key only). Matches objects where _rperm is empty or doesn't exist.

Examples:

Song.query.privately_readable.results
Song.query.master_key_read_only.results  # Alias

Parameters:

  • mongo_direct (Boolean) (defaults to: nil)

    if true, forces MongoDB direct query.

Returns:



5713
5714
5715
# File 'lib/parse/query.rb', line 5713

def privately_readable(mongo_direct: nil)
  readable_by("none", mongo_direct: mongo_direct)
end

#privately_writable(mongo_direct: nil) ⇒ Parse::Query Also known as: master_key_write_only

Find objects with no write permissions (master key only). Matches objects where _wperm is empty or doesn't exist.

Examples:

Song.query.privately_writable.results
Song.query.master_key_write_only.results  # Alias

Parameters:

  • mongo_direct (Boolean) (defaults to: nil)

    if true, forces MongoDB direct query.

Returns:



5727
5728
5729
# File 'lib/parse/query.rb', line 5727

def privately_writable(mongo_direct: nil)
  writable_by("none", mongo_direct: mongo_direct)
end

#publicly_readable(mongo_direct: nil) ⇒ Parse::Query

Find objects that are publicly readable (anyone can read). Matches objects where _rperm contains "*".

Examples:

Song.query.publicly_readable.results
Song.query.publicly_readable.where(genre: "Rock").results

Parameters:

  • mongo_direct (Boolean) (defaults to: nil)

    if true, forces MongoDB direct query.

Returns:



5689
5690
5691
# File 'lib/parse/query.rb', line 5689

def publicly_readable(mongo_direct: nil)
  readable_by("*", mongo_direct: mongo_direct)
end

#publicly_writable(mongo_direct: nil) ⇒ Parse::Query

Find objects that are publicly writable (anyone can write). Matches objects where _wperm contains "*". Useful for security audits to find potentially insecure objects.

Examples:

Song.query.publicly_writable.results  # Security audit!

Parameters:

  • mongo_direct (Boolean) (defaults to: nil)

    if true, forces MongoDB direct query.

Returns:



5701
5702
5703
# File 'lib/parse/query.rb', line 5701

def publicly_writable(mongo_direct: nil)
  writable_by("*", mongo_direct: mongo_direct)
end

#raise_scoped_aggregation_requires_mongo_direct!Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Fail closed for a scoped aggregation that would otherwise fall back to REST /aggregate. That endpoint is master-key-only and enforces neither ACL nor CLP, so letting a scoped query through would silently run it unscoped as the master key. Every aggregation terminal that routes a scoped query (aggregate, aggregate_from_query, count, execute_aggregation_pipeline) raises through here.



1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
# File 'lib/parse/query.rb', line 1832

def raise_scoped_aggregation_requires_mongo_direct!
  raise MongoDirectRequired,
    "[Parse::Query] This scoped aggregation (session_token / " \
    "scope_to_user / scope_to_role) requires mongo-direct so the " \
    "SDK can enforce ACL/CLP. Parse Server's REST /aggregate " \
    "endpoint is master-key-only and enforces neither, so routing " \
    "it there would silently run unscoped as the master key. " \
    "Enable mongo-direct via Parse::MongoDB.configure(...), or " \
    "rewrite without the aggregation terminal."
end

#raw { ... } ⇒ Array<Hash>

Returns raw unprocessed results from the query (hash format)

Yields:

  • a block to iterate for each raw object that matched the query

Returns:

  • (Array<Hash>)

    raw Parse JSON hash results



2065
2066
2067
# File 'lib/parse/query.rb', line 2065

def raw(&block)
  results(raw: true, &block)
end

#read_pref(preference) ⇒ self

Set the MongoDB read preference for this query. This allows directing read queries to secondary replicas for load balancing.

Examples:

Song.query.read_preference(:secondary).results
Song.query.read_preference(:nearest).results

Parameters:

  • preference (Symbol, String)

    the read preference. Valid values: :primary, :primary_preferred, :secondary, :secondary_preferred, :nearest

Returns:

  • (self)


849
850
851
852
# File 'lib/parse/query.rb', line 849

def read_pref(preference)
  @read_preference = preference
  self
end

#readable_by(permission, mongo_direct: nil, strict: false) ⇒ Parse::Query

Note:

This uses MongoDB aggregation pipeline because Parse Server restricts direct queries on internal ACL fields (_rperm/_wperm).

Filter by ACL read permissions using exact permission strings. Strings are used as-is (user IDs or "role:RoleName" format). Use "public" for public access, "none" or [] for no read permissions.

Examples:

Song.query.readable_by("user123")               # readable by user ID (+ public)
Song.query.readable_by("role:Admin")            # readable by Admin role (+ public)
Song.query.readable_by(current_user)            # by user object, roles expanded (+ public)
Song.query.readable_by(:public)                 # publicly readable objects
Song.query.readable_by("none")                  # objects with no read permissions
Song.query.readable_by([])                      # objects with no read permissions (empty ACL)
Song.query.readable_by("role:Admin", strict: true)  # ONLY rows that explicitly grant Admin

Parameters:

  • permission (Parse::User, Parse::Role, Parse::Pointer, String, Symbol, Array)

    the permission to check. A Parse::User (or User pointer) expands to the user's objectId plus every role they inherit; a Parse::Role (or role name String / :ACL.readable_by_role form) expands up the role hierarchy. "public" / :public / :everyone / :world map to the "*" wildcard. "none" / :none / [] / nil match objects with no read permissions (explicit empty _rperm).

  • mongo_direct (Boolean) (defaults to: nil)

    if true, forces MongoDB direct query. If nil (default), auto-detects based on query complexity. Set to false to force Parse Server aggregation.

  • strict (Boolean) (defaults to: false)

    when false (default), the match is inclusive: it ALSO returns publicly-readable rows (_rperm contains "*") and rows with a missing _rperm (public by absence), because those are genuinely readable by the principal. This is access-simulation semantics ("what can this principal read"). Pass strict: true for an exact match — only rows whose _rperm literally contains one of the resolved permissions, with no public/missing rows — which is what an ownership or security audit wants ("which rows explicitly grant this principal"). Equivalent to the :ACL.readable_by_exact operator.

Returns:



5609
5610
5611
5612
5613
# File 'lib/parse/query.rb', line 5609

def readable_by(permission, mongo_direct: nil, strict: false)
  @acl_query_mongo_direct = mongo_direct unless mongo_direct.nil?
  where((strict ? :ACL.readable_by_exact : :ACL.readable_by) => permission)
  self
end

#readable_by_role(role_name, mongo_direct: nil, strict: false) ⇒ Parse::Query

Filter by ACL read permissions using role names (adds "role:" prefix).

Examples:

Song.query.readable_by_role("Admin")              # Objects readable by Admin role
Song.query.readable_by_role(["Admin", "Editor"])  # Objects readable by Admin or Editor
Song.query.readable_by_role(admin_role)           # Objects readable by Role object

Parameters:

  • role_name (Parse::Role, String, Array)

    the role name(s) to check

  • mongo_direct (Boolean) (defaults to: nil)

    if true, forces MongoDB direct query.

  • strict (Boolean) (defaults to: false)

    when true, exact match only — no implicit public "*" and no missing-_rperm rows. See #readable_by.

Returns:



5626
5627
5628
5629
5630
# File 'lib/parse/query.rb', line 5626

def readable_by_role(role_name, mongo_direct: nil, strict: false)
  @acl_query_mongo_direct = mongo_direct unless mongo_direct.nil?
  where((strict ? :ACL.readable_by_role_exact : :ACL.readable_by_role) => role_name)
  self
end

Raises:

  • (ArgumentError)


879
880
881
882
883
# File 'lib/parse/query.rb', line 879

def related_to(field, pointer)
  raise ArgumentError, "Object value must be a Parse::Pointer type" unless pointer.is_a?(Parse::Pointer)
  add_constraint field.to_sym.related_to, pointer
  self #chaining
end

#requires_aggregation?Boolean

Check if this query requires aggregation pipeline execution

Returns:

  • (Boolean)

    true if the query contains pipeline-based constraints



4339
4340
4341
# File 'lib/parse/query.rb', line 4339

def requires_aggregation?
  !pipeline.empty?
end

#requires_aggregation_pipeline?Boolean

Check if this query contains constraints that require aggregation pipeline processing

Returns:

  • (Boolean)

    true if aggregation pipeline is required



2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
# File 'lib/parse/query.rb', line 2046

def requires_aggregation_pipeline?
  return false if @where.empty?

  # Markers (including __aggregation_pipeline) are stripped from the
  # public compile_where path; consult the marker view explicitly.
  markers = compile_markers

  # Check if the marker hash itself has aggregation pipeline marker
  return true if markers.key?("__aggregation_pipeline")

  # Check if any of the constraint values has aggregation pipeline marker
  markers.values.any? { |constraint|
    constraint.is_a?(Hash) && constraint.key?("__aggregation_pipeline")
  }
end

#requires_mongo_direct?Boolean

Check if this query contains a constraint that can only be answered via mongo-direct (e.g. $geoIntersects with a full $geometry against a non-GeoPoint column — an operator Parse Server's REST find layer does not expose). Direct-only constraints emit a "__mongo_direct_only" marker which this predicate detects.

Returns:

  • (Boolean)


1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
# File 'lib/parse/query.rb', line 1800

def requires_mongo_direct?
  return false if @where.empty?
  # Read from the un-stripped marker hash — `compile_where` removes
  # `__`-prefixed routing markers before they ship to Parse / Mongo.
  markers = compile_markers
  return true if markers.key?("__mongo_direct_only")
  markers.values.any? do |constraint|
    constraint.is_a?(Hash) && constraint.key?("__mongo_direct_only")
  end
end

#result_pointers { ... } ⇒ Array<Parse::Pointer> Also known as: results_pointers

Returns only pointer objects for all matching results This is memory efficient for large result sets where you only need pointers

Yields:

  • a block to iterate for each pointer object that matched the query

Returns:



2073
2074
2075
# File 'lib/parse/query.rb', line 2073

def result_pointers(&block)
  results(return_pointers: true, &block)
end

#results(raw: false, return_pointers: false, mongo_direct: false) { ... } ⇒ Array<Hash>, Array<Parse::Object> Also known as: result

Executes the query and builds the result set of Parse::Objects that matched. When this method is passed a block, the block is yielded for each matching item in the result, and the items are not returned. This methodology is more performant as large quantifies of objects are fetched in batches and all of them do not have to be kept in memory after the query finishes executing. This is the recommended method of processing large result sets.

Examples:

query = Parse::Query.new("_User", :created_at.before => DateTime.now)
users = query.results # => Array of Parse::User objects.

query = Parse::Query.new("_User", limit: :max)

query.results do |user|
 # recommended; more memory efficient
end

Parameters:

  • raw (Boolean) (defaults to: false)

    whether to get the raw hash results of the query instead of a set of Parse::Object subclasses.

  • mongo_direct (Boolean) (defaults to: false)

    if true, queries MongoDB directly bypassing Parse Server. Requires Parse::MongoDB to be configured. Default: false.

Yields:

  • a block to iterate for each object that matched the query.

Returns:

  • (Array<Hash>)

    if raw is set to true, a set of Parse JSON hashes.

  • (Array<Parse::Object>)

    if raw is set to false, a list of matching Parse::Object subclasses.



1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
# File 'lib/parse/query.rb', line 1724

def results(raw: false, return_pointers: false, mongo_direct: false, &block)
  # Use direct MongoDB query if requested
  if mongo_direct
    return results_direct(raw: raw, **mongo_direct_auth_kwargs, &block)
  end

  # Auto-route to mongo-direct when the compiled where contains a
  # constraint that Parse Server's REST find layer cannot express
  # (e.g. $geoIntersects with a full $geometry against a non-Point
  # column). Mirrors the existing aggregation auto-route at line
  # ~1321 below — the constraint emits a marker, the query layer
  # detects it, and routing happens transparently. The auth
  # context (use_master_key, scope_to_user, or session_token)
  # decides how ACL simulation runs through mongo-direct.
  if requires_mongo_direct?
    assert_mongo_direct_routable!
    return results_direct(raw: raw, **mongo_direct_auth_kwargs, &block)
  end

  if @results.nil?
    if block_given?
      max_results(raw: raw, return_pointers: return_pointers, &block)
    elsif @limit.is_a?(Numeric) || requires_aggregation_pipeline?
      # Check if this query requires aggregation pipeline processing
      if requires_aggregation_pipeline?
        # Use Aggregation class which handles both Parse Server and MongoDB direct
        aggregation = execute_aggregation_pipeline
        if raw
          items = aggregation.raw
        elsif return_pointers
          items = to_pointers(aggregation.raw)
        else
          items = aggregation.results
        end
        return items.each(&block) if block_given?
        @results = items
      else
        response = fetch!(compile)
        return [] if response.error?
        items = if raw
            response.results
          elsif return_pointers
            to_pointers(response.results)
          else
            decode(response.results)
          end
        return items.each(&block) if block_given?
        @results = items
      end
    else
      @results = max_results(raw: raw, return_pointers: return_pointers)
    end
  end
  @results
end

#results_direct(raw: false, max_time_ms: nil, session_token: nil, master: nil, acl_user: nil, acl_role: nil) { ... } ⇒ Array<Parse::Object>, Array<Hash>

Note:

This is a read-only operation. Direct MongoDB queries cannot modify data.

Execute the query directly against MongoDB, bypassing Parse Server. This is useful for performance-critical read operations.

Examples:

Basic usage

songs = Song.query(:plays.gt => 1000).results_direct

With raw results

raw_docs = Song.query(:artist => "Beatles").results_direct(raw: true)

Parameters:

  • raw (Boolean) (defaults to: false)

    if true, returns raw MongoDB documents converted to Parse format instead of Parse::Object instances (default: false)

  • max_time_ms (Integer, nil) (defaults to: nil)

    optional server-side time limit in milliseconds. When provided, MongoDB will cancel the aggregation if it exceeds this budget and MongoDB::ExecutionTimeout is raised. Pass +nil+ (the default) for no cap.

Yields:

  • a block to iterate for each object that matched the query

Returns:

  • (Array<Parse::Object>)

    if raw is false, a list of Parse::Object subclasses

  • (Array<Hash>)

    if raw is true, Parse-formatted JSON hashes

Raises:

See Also:



2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
# File 'lib/parse/query.rb', line 2102

def results_direct(raw: false, max_time_ms: nil, session_token: nil, master: nil, acl_user: nil, acl_role: nil, &block)
  require_relative "mongodb"
  Parse::MongoDB.require_gem!

  unless Parse::MongoDB.available?
    raise Parse::MongoDB::NotEnabled,
      "Direct MongoDB queries are not enabled. " \
      "Call Parse::MongoDB.configure(uri: 'mongodb://...', enabled: true) first."
  end

  # Build the aggregation pipeline for direct MongoDB execution
  pipeline = build_direct_mongodb_pipeline

  # When no explicit auth kwargs are provided by the caller, derive them
  # from the query's own auth state (session_token, acl_user, acl_role, or
  # master key) via mongo_direct_auth_kwargs — exactly the same fallback
  # used by distinct_direct, count_direct, and the requires_mongo_direct?
  # auto-route in results(). Without this, a plain .results_direct call on
  # a master-key client would resolve as anonymous and have the ACL match
  # stage filter out every row whose _rperm is [] (the default for objects
  # created without an explicit public-read ACL).
  if session_token.nil? && master.nil? && acl_user.nil? && acl_role.nil?
    auth = mongo_direct_auth_kwargs
    session_token = auth[:session_token]
    master        = auth[:master]
    acl_user      = auth[:acl_user]
    acl_role      = auth[:acl_role]
  end

  # Execute the aggregation directly on MongoDB. The pipeline was built
  # entirely from SDK constraint translation (no user-supplied stages),
  # so legitimate +_rperm+/+_wperm+ references emitted by
  # {#readable_by_role} and friends are sanctioned. The DENIED_OPERATORS
  # walk still runs at the MongoDB layer. When `session_token:` or
  # `master:` is supplied, Parse::MongoDB.aggregate adds the
  # three-layer ACL simulation (top-level $match, $lookup rewriter,
  # post-fetch redactor) before/after the pipeline executes.
  raw_results = Parse::MongoDB.aggregate(@table, pipeline,
                                         max_time_ms: max_time_ms,
                                         allow_internal_fields: true,
                                         session_token: session_token,
                                         master: master,
                                         acl_user: acl_user,
                                         acl_role: acl_role,
                                         read_preference: @read_preference,
                                         hint: @hint)

  # Convert MongoDB documents to Parse format
  parse_results = Parse::MongoDB.convert_documents_to_parse(raw_results, @table)

  # Honor exclude_keys on the mongo-direct path: the pipeline can only
  # project the keys allowlist, so apply the denylist here as a post-fetch
  # sanitize over the Parse-format hashes (before the raw/decode fork so
  # both shapes are redacted). Does not alter the MongoDB query.
  redact_excluded_keys!(parse_results)

  if raw
    return parse_results.each(&block) if block_given?
    return parse_results
  end

  # Convert to Parse objects
  items = decode(parse_results)
  return items.each(&block) if block_given?
  items
end

#rewrite_expression_for_direct_mongodb(expr) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Recursively rewrite field references inside an aggregation expression to their direct-MongoDB column names.

Walks Strings, Arrays, and Hashes:

  • A String starting with $ (but not $$, which denotes a let variable or system variable like $$ROOT) is treated as a field reference. Its root path segment is rewritten via #convert_field_for_direct_mongodb, preserving any dot-delimited tail. Already-rewritten $_p_* references pass through unchanged.
  • Arrays and Hashes are recursed into, with one exception: the argument of $literal is a string constant, not a field reference, and must not be rewritten.

Parameters:

  • expr (Object)

    any node within an aggregation expression

Returns:

  • (Object)

    the rewritten expression (input is not mutated)



3172
3173
3174
3175
3176
3177
3178
3179
3180
3181
3182
3183
3184
3185
3186
3187
3188
3189
3190
3191
3192
3193
3194
3195
3196
# File 'lib/parse/query.rb', line 3172

def rewrite_expression_for_direct_mongodb(expr)
  case expr
  when String
    return expr unless expr.start_with?("$")
    # $$varName (let bindings) and $$ROOT / $$CURRENT / $$NOW etc.
    return expr if expr.start_with?("$$")
    # Split off the root path segment so `$user.name` rewrites only
    # the root: `$_p_user.name`. Internal helper handles _p_* and
    # built-in passthroughs idempotently.
    head, sep, tail = expr[1..-1].partition(".")
    "$#{convert_field_for_direct_mongodb(head)}#{sep}#{tail}"
  when Array
    expr.map { |e| rewrite_expression_for_direct_mongodb(e) }
  when Hash
    result = {}
    expr.each do |k, v|
      # `$literal` wraps a string constant; its argument is not a
      # field reference and must be preserved verbatim.
      result[k] = k.to_s == "$literal" ? v : rewrite_expression_for_direct_mongodb(v)
    end
    result
  else
    expr
  end
end

#scope_to_role(role) ⇒ self

Role-based ACL scoping for service-account-style queries that need "what would a user holding this role see" without minting a session token or naming a specific user. The SDK uses Parse::Role#all_parent_role_names to expand the role's inheritance chain so passing "scope:admin" includes any role "scope:admin" inherits from (e.g. "scope:user").

The resulting permission set is ["*", "role:<name>", ...] — no user_id slot. Documents whose _rperm would only grant a specific user (and not any of the role names) are filtered out of both the top-level result set and embedded sub-documents.

Same routing rules as #scope_to_user: the query auto-routes through mongo-direct when the where clause contains a direct-only constraint, and the three-layer ACL simulation (top-level $match, $lookup rewriter, post-fetch redactor) runs through ACLScope.

Examples:

Region.query(:area.geo_intersects => route)
      .scope_to_role("scope:admin")
      .results

Parameters:

  • role (Parse::Role, String)

    role to scope by. Strings may be supplied with or without the "role:" prefix; the SDK strips it. Unknown role names raise ArgumentError at first use.

Returns:

  • (self)


1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
# File 'lib/parse/query.rb', line 1915

def scope_to_role(role)
  unless role.is_a?(Parse::Role) || role.is_a?(String) || role.is_a?(Symbol)
    raise ArgumentError, "[Parse::Query] scope_to_role requires a Parse::Role or role-name String."
  end
  # Normalize Symbol at the boundary so downstream
  # Parse::ACLScope#resolve_for_role only ever sees Parse::Role or
  # String. Without normalization, any String-only operation
  # (e.g. #start_with?, #sub) silently NoMethodErrors on Symbol.
  @acl_role = role.is_a?(Symbol) ? role.to_s : role
  self
end

#scope_to_user(user) ⇒ self

Scope a query to a specific user's row-level ACL when it auto-routes through mongo-direct. The SDK records the user, computes the effective _rperm allow-set (user objectId + "*" + every role name the user inherits via Role.all_for_user), and prepends a { _rperm: { $in: ... } } $match to the mongo-direct pipeline at execution time.

What this does NOT replicate: class-level permissions (CLP), anonymous-user public-access nuances, beforeFind/afterFind cloud triggers, or any field-level redaction Parse Server might otherwise apply. This is a row-ACL floor, not full enforcement parity with the Parse Server REST path. The intended use case is "I need this mongo-direct-only query from a session-tokened context, and I accept the row-ACL floor as my filter."

Edge case — objects with missing _rperm: Parse Server only writes _rperm when an explicit ACL is applied; rows saved with master-key access and no explicit ACL leave the field unset. The injected filter is {$or: [{_rperm: {$exists: false}}, {_rperm: {$in: perms}}]}, treating missing-_rperm rows as public-readable. Apps that store row-level ACL on every object are unaffected by this fallback; apps that mix ACL'd and public-default rows will see both classes of row through the scoped query.

The query MUST still satisfy #assert_mongo_direct_routable! — either use_master_key: true OR scope_to_user is set. A call to scope_to_user is treated as opt-in to mongo-direct routing for the direct-only constraints in the where clause.

Examples:

Region.query(:area.geo_intersects => route)
      .scope_to_user(current_user)
      .results

Parameters:

Returns:

  • (self)

Raises:

  • (ArgumentError)


1880
1881
1882
1883
1884
1885
# File 'lib/parse/query.rb', line 1880

def scope_to_user(user)
  raise ArgumentError, "[Parse::Query] scope_to_user requires a Parse::User or User Pointer." \
    unless user.respond_to?(:id) && user.id.is_a?(String)
  @acl_user = user
  self
end

#select { ... } ⇒ Array

Yields:

  • a block yield for each object in the result

Returns:

See Also:

  • Array#select


1368
1369
1370
1371
# File 'lib/parse/query.rb', line 1368

def select(&block)
  return results.enum_for(:select) unless block_given? # Sparkling magic!
  results.select(&block)
end

#skip(amount) ⇒ self

Use with limit to paginate through results. Default is 0.

Examples:

# get the next 3 songs after the first 10
Song.all :limit => 3, :skip => 10

Parameters:

  • amount (Integer)

    The number of records to skip.

Returns:

  • (self)


778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
# File 'lib/parse/query.rb', line 778

def skip(amount)
  coerced =
    case amount
    when nil      then 0
    when Numeric  then amount.to_i
    when String
      unless amount =~ /\A-?\d+\z/
        raise ArgumentError,
              "Invalid skip #{amount.inspect}. Expected an Integer, " \
              "a numeric String, or nil."
      end
      amount.to_i
    else
      raise ArgumentError,
            "Invalid skip #{amount.inspect}. Expected an Integer, " \
            "a numeric String, or nil."
    end
  @skip = [0, coerced].max
  @results = nil
  self #chaining
end

#subscribe(fields: nil, keys: nil, watch: nil, session_token: nil, client: nil, use_master_key: false) {|subscription| ... } ⇒ Parse::LiveQuery::Subscription

Subscribe to real-time updates for objects matching this query. Uses Parse LiveQuery WebSocket connection to receive push notifications when objects are created, updated, deleted, or enter/leave the query results.

Examples:

Basic subscription

subscription = Song.query(:artist => "Beatles").subscribe
subscription.on(:create) { |song| puts "New song: #{song.title}" }
subscription.on(:update) { |song, original| puts "Updated!" }
subscription.on(:delete) { |song| puts "Deleted: #{song.id}" }

With field filtering

subscription = User.query(:status => "active").subscribe(fields: ["name", "email"])
subscription.on_update { |user| puts "User updated: #{user.name}" }

With session token for ACL-aware subscriptions

subscription = PrivateData.query.subscribe(session_token: current_user.session_token)

Parameters:

  • fields (Array<String>) (defaults to: nil)

    specific fields to watch for changes (nil = all fields)

  • session_token (String) (defaults to: nil)

    session token for ACL-aware subscriptions

  • client (Parse::LiveQuery::Client) (defaults to: nil)

    custom LiveQuery client (optional)

  • use_master_key (Boolean) (defaults to: false)

    an intent assertion, NOT a per-subscription elevation. Parse Server resolves masterKey once, at connect time, from the LiveQuery connect frame; the subscribe frame never carries it. This flag therefore only has effect when the underlying client is itself an admin connection (Parse::LiveQuery::Client.new(use_master_key: true) with a master key), in which case the entire socket is already elevated and ALL its subscriptions bypass ACL/CLP. On a non-admin connection use_master_key: true does not elevate the subscription and emits a security warning. A single socket cannot mix scoped and admin subscriptions — use separate connections for end-user (session-token-scoped) versus administrative (master-key-scoped) work.

Yields:

  • (subscription)

    runs the block with the freshly-constructed LiveQuery::Subscription BEFORE the subscribe frame is sent so caller-registered callbacks are wired before any server events can arrive. Optional.

Returns:

See Also:



3267
3268
3269
3270
3271
3272
3273
3274
3275
3276
3277
3278
3279
3280
3281
# File 'lib/parse/query.rb', line 3267

def subscribe(fields: nil, keys: nil, watch: nil, session_token: nil, client: nil, use_master_key: false, &block)
  require_relative "live_query"

  lq_client = client || Parse::LiveQuery.client
  lq_client.subscribe(
    @table,
    where: compile_where,
    fields: fields,
    keys: keys,
    watch: watch,
    session_token: session_token || @session_token,
    use_master_key: use_master_key,
    &block
  )
end

#sum(field) ⇒ Numeric

Calculate the sum of values for a specific field.

Parameters:

Returns:

  • (Numeric)

    the sum of all values for the field, or 0 if no results.



4352
4353
4354
4355
4356
4357
4358
4359
4360
4361
4362
4363
4364
4365
4366
# File 'lib/parse/query.rb', line 4352

def sum(field)
  if field.nil? || !field.respond_to?(:to_s)
    raise ArgumentError, "Invalid field name passed to `sum`."
  end

  # Format field name according to Parse conventions
  formatted_field = format_aggregation_field(field)

  # Build the aggregation pipeline
  pipeline = [
    { "$group" => { "_id" => nil, "total" => { "$sum" => "$#{formatted_field}" } } },
  ]

  execute_basic_aggregation(pipeline, "sum", field, "total")
end

#to_aArray

Returns:

See Also:

  • Array#to_a


1375
1376
1377
# File 'lib/parse/query.rb', line 1375

def to_a
  results.to_a
end

#to_pointers(list, field = nil) ⇒ Array<Parse::Pointer>

Builds Parse::Pointer objects based on the set of Parse JSON hashes in an array.

Parameters:

  • list (Array<Hash>)

    a list of Parse JSON hashes

  • field (Symbol, String, nil) (defaults to: nil)

    optional field name for schema-based conversion

Returns:



4200
4201
4202
4203
4204
4205
4206
4207
4208
4209
4210
4211
4212
4213
4214
4215
4216
4217
4218
4219
4220
4221
4222
4223
4224
4225
4226
4227
4228
4229
4230
4231
4232
4233
4234
4235
# File 'lib/parse/query.rb', line 4200

def to_pointers(list, field = nil)
  list.map do |m|
    if field
      # Use schema-based conversion when field is provided
      converted = convert_pointer_value_with_schema(m, field, return_pointers: true)
      if converted.is_a?(Parse::Pointer)
        converted
      elsif m.is_a?(String) && m.include?("$")
        # Fallback to string parsing if schema conversion didn't work
        class_name, object_id = m.split("$", 2)
        if class_name && object_id
          Parse::Pointer.new(class_name, object_id)
        end
      else
        nil
      end
    else
      # Original logic for backward compatibility
      if m.is_a?(Hash)
        if m["__type"] == "Pointer" && m["className"] && m["objectId"]
          # Parse pointer object - use the className from the pointer
          Parse::Pointer.new(m["className"], m["objectId"])
        elsif m["objectId"]
          # Standard Parse object with objectId - use the query table name
          Parse::Pointer.new(@table, m["objectId"])
        end
      elsif m.is_a?(String) && m.include?("$")
        # Handle MongoDB pointer string format: "ClassName$objectId"
        class_name, object_id = m.split("$", 2)
        if class_name && object_id
          Parse::Pointer.new(class_name, object_id)
        end
      end
    end
  end.compact
end

#to_table(columns = nil, format: :ascii, headers: nil, sort_by: nil, sort_order: :asc) ⇒ String

Convert query results to a formatted table display.

Examples:

# Basic usage with object fields
Project.query.to_table([:object_id, :name, :address])

# With dot notation for related objects
Document.query.to_table([
  :object_id,
  "project.name",        # Access project name through relationship
  "project.workspace.name",   # Access workspace name through project->workspace relationship
  :file_size
])

# With custom headers and calculated columns
Project.query.to_table([
  { field: :object_id, header: "ID" },
  { field: "workspace.name", header: "Workspace Name" },
  { field: :address, header: "Project Address" },
  { block: ->(proj) { proj.notes.count }, header: "Note Count" }
])

# Your specific example:
Project.query.to_table([
  :object_id,
  { field: :name, header: "Project Name" },
  { field: :address, header: "Project Address" },
  { block: ->(p) { p.notes&.count || 0 }, header: "Note Count" }
])

Parameters:

  • columns (Array<Symbol, String, Hash>) (defaults to: nil)

    column definitions. Can be:

    • Symbol/String: field name (e.g., :object_id, :name) or dot notation (e.g., "project.workspace.name")
    • Hash: { field: :custom_name, header: "Custom Header" }
    • Hash: { block: ->(obj) { obj.some_calculation }, header: "Calculated" }
  • format (Symbol) (defaults to: :ascii)

    output format (:ascii, :csv, :json)

  • headers (Array<String>) (defaults to: nil)

    custom headers (overrides auto-generated ones)

Returns:

  • (String)

    formatted table



4567
4568
4569
4570
4571
4572
4573
4574
4575
4576
4577
4578
4579
4580
4581
4582
4583
4584
4585
4586
4587
4588
4589
4590
4591
4592
4593
4594
4595
# File 'lib/parse/query.rb', line 4567

def to_table(columns = nil, format: :ascii, headers: nil, sort_by: nil, sort_order: :asc)
  objects = results
  return format_empty_table(format) if objects.empty?

  # Auto-detect columns if not provided
  if columns.nil?
    columns = auto_detect_columns(objects.first)
  end

  # Build table data
  table_data = build_table_data(objects, columns, headers)

  # Sort table data if sort_by is specified
  if sort_by
    sort_table_data!(table_data, sort_by, sort_order)
  end

  # Format based on requested format
  case format
  when :ascii
    format_ascii_table(table_data)
  when :csv
    format_csv_table(table_data)
  when :json
    format_json_table(table_data)
  else
    raise ArgumentError, "Unsupported format: #{format}. Use :ascii, :csv, or :json"
  end
end

#translate_pipeline_for_direct_mongodb(pipeline) ⇒ Array<Hash>

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Apply the direct-MongoDB stage converter to every stage in a pipeline. Idempotent on already-translated input (the per-stage converter passes _p_* references through unchanged).

Parameters:

  • pipeline (Array<Hash>)

    aggregation pipeline

Returns:

  • (Array<Hash>)

    a new pipeline with each stage translated



3570
3571
3572
3573
# File 'lib/parse/query.rb', line 3570

def translate_pipeline_for_direct_mongodb(pipeline)
  return pipeline unless pipeline.is_a?(Array)
  pipeline.map { |stage| convert_stage_for_direct_mongodb(stage) }
end

#validate_no_where_operator!(hash) ⇒ Object

Deprecated.

Retained for backwards compatibility. Use PipelineSecurity.validate_filter! for new code.

Parameters:

  • hash (Hash)

    the hash to check.

Raises:

  • (ArgumentError)

    if $where (or any other denied operator) is found.



3601
3602
3603
3604
3605
# File 'lib/parse/query.rb', line 3601

def validate_no_where_operator!(hash)
  Parse::PipelineSecurity.validate_filter!(hash)
rescue Parse::PipelineSecurity::Error => e
  raise ArgumentError, e.message
end

#validate_pipeline!(pipeline) ⇒ Object

Note:

Permissive mode does NOT block $lookup, $graphLookup, or $unionWith — these are legitimate read stages but can cross collection boundaries that Parse ACL/CLP does not enforce. Do not pass raw attacker-controlled input into #aggregate; construct the pipeline in SDK code and interpolate only validated values.

Validates that a pipeline does not contain dangerous operators. Uses the permissive mode of PipelineSecurity (recursive denylist for $where, $function, $accumulator, $out, $merge, $collMod, $createIndex, $dropIndex) so that user code passing uncommon-but-legitimate read stages like $densify or $fill continues to work. Strict allowlist validation is available via PipelineSecurity.validate_pipeline! for callers that want to opt in.

Parameters:

  • pipeline (Array<Hash>)

    the aggregation pipeline stages.

Raises:

  • (ArgumentError)

    if a blocked stage or dangerous operator is found.



3591
3592
3593
3594
3595
# File 'lib/parse/query.rb', line 3591

def validate_pipeline!(pipeline)
  Parse::PipelineSecurity.validate_filter!(pipeline)
rescue Parse::PipelineSecurity::Error => e
  raise ArgumentError, e.message
end

#where(expressions = nil, opts = {}) ⇒ self

Add additional query constraints to the where clause. The where clause is based on utilizing a set of constraints on the defined column names in your Parse classes. The constraints are implemented as method operators on field names that are tied to a value. Any symbol/string that is not one of the main expression keywords described here will be considered as a type of query constraint for the where clause in the query.

Examples:

# parts of a single where constraint
{ :column.constraint => value }

Parameters:

  • conditions (Hash)

    a set of constraints for this query.

  • opts (Hash) (defaults to: {})

    a set of options when adding the constraints. This is specific for each Parse::Constraint.

Returns:

  • (self)

See Also:



1036
1037
1038
1039
1040
1041
1042
1043
# File 'lib/parse/query.rb', line 1036

def where(expressions = nil, opts = {})
  return @where if expressions.nil?
  if expressions.is_a?(Hash)
    # Route through conditions to handle special keywords like :keys, :include, etc.
    conditions(expressions)
  end
  self #chaining
end

#where_constraintsHash

Formats the current set of Parse::Constraint instances in the where clause as an expression hash.

Returns:

  • (Hash)

    the set of constraints



1018
1019
1020
# File 'lib/parse/query.rb', line 1018

def where_constraints
  @where.reduce({}) { |memo, constraint| memo[constraint.operation] = constraint.value; memo }
end

#writable_by(permission, mongo_direct: nil, strict: false) ⇒ Parse::Query

Note:

This uses MongoDB aggregation pipeline because Parse Server restricts direct queries on internal ACL fields (_rperm/_wperm).

Filter by ACL write permissions using exact permission strings. Strings are used as-is (user IDs or "role:RoleName" format). Use "public" for public access, "none" or [] for no write permissions.

Examples:

Song.query.writable_by("user123")               # writable by user ID (+ public)
Song.query.writable_by("role:Admin")            # writable by Admin role (+ public)
Song.query.writable_by(current_user)            # by user object, roles expanded (+ public)
Song.query.writable_by(:public)                 # Publicly writable objects
Song.query.writable_by("none")                  # objects with no write permissions
Song.query.writable_by([])                      # objects with no write permissions (empty ACL)
Song.query.writable_by("role:Admin", strict: true)  # ONLY rows that explicitly grant Admin

Parameters:

  • permission (Parse::User, Parse::Role, Parse::Pointer, String, Symbol, Array)

    the permission to check. See #readable_by for value coercion and role expansion.

  • mongo_direct (Boolean) (defaults to: nil)

    if true, forces MongoDB direct query. If nil (default), auto-detects based on query complexity. Set to false to force Parse Server aggregation.

  • strict (Boolean) (defaults to: false)

    when true, exact match only — no implicit public "*" and no missing-_wperm rows. See #readable_by.

Returns:



5654
5655
5656
5657
5658
# File 'lib/parse/query.rb', line 5654

def writable_by(permission, mongo_direct: nil, strict: false)
  @acl_query_mongo_direct = mongo_direct unless mongo_direct.nil?
  where((strict ? :ACL.writable_by_exact : :ACL.writable_by) => permission)
  self
end

#writable_by_role(role_name, mongo_direct: nil, strict: false) ⇒ Parse::Query

Filter by ACL write permissions using role names (adds "role:" prefix).

Examples:

Song.query.writable_by_role("Admin")              # Objects writable by Admin role
Song.query.writable_by_role(["Admin", "Editor"])  # Objects writable by Admin or Editor
Song.query.writable_by_role(admin_role)           # Objects writable by Role object

Parameters:

  • role_name (Parse::Role, String, Array)

    the role name(s) to check

  • mongo_direct (Boolean) (defaults to: nil)

    if true, forces MongoDB direct query.

  • strict (Boolean) (defaults to: false)

    when true, exact match only — no implicit public "*" and no missing-_wperm rows. See #readable_by.

Returns:



5671
5672
5673
5674
5675
# File 'lib/parse/query.rb', line 5671

def writable_by_role(role_name, mongo_direct: nil, strict: false)
  @acl_query_mongo_direct = mongo_direct unless mongo_direct.nil?
  where((strict ? :ACL.writable_by_role_exact : :ACL.writable_by_role) => role_name)
  self
end

#|(other_query) ⇒ Query

Returns the combined query with an OR clause.

Returns:

  • (Query)

    the combined query with an OR clause.

Raises:

  • (ArgumentError)

See Also:



1091
1092
1093
1094
1095
1096
# File 'lib/parse/query.rb', line 1091

def |(other_query)
  raise ArgumentError, "Parse queries must be of the same class #{@table}." unless @table == other_query.table
  copy_query = self.clone
  copy_query.or_where other_query.where
  copy_query
end