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



5561
5562
5563
5564
5565
5566
5567
5568
5569
5570
5571
5572
5573
5574
5575
5576
5577
5578
5579
5580
5581
5582
5583
5584
5585
5586
5587
5588
5589
5590
5591
5592
# File 'lib/parse/query.rb', line 5561

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



5526
5527
5528
5529
5530
5531
5532
5533
5534
5535
5536
5537
5538
5539
5540
5541
5542
5543
5544
5545
5546
5547
5548
5549
5550
5551
5552
5553
5554
# File 'lib/parse/query.rb', line 5526

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



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
3563
3564
3565
3566
3567
3568
3569
3570
3571
3572
3573
3574
3575
3576
3577
3578
3579
3580
3581
3582
3583
3584
3585
3586
3587
3588
3589
3590
3591
3592
3593
3594
3595
3596
3597
3598
3599
3600
3601
3602
3603
3604
3605
3606
3607
3608
# File 'lib/parse/query.rb', line 3416

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?
  mongo_ready = defined?(Parse::MongoDB) && Parse::MongoDB.enabled?
  use_mongo_direct = mongo_direct

  if scoped
    # A scoped aggregation (session_token / scope_to_user / scope_to_role)
    # must NEVER reach Parse Server's REST /aggregate endpoint — it is
    # master-key-only and enforces NEITHER ACL NOR CLP, so it would run
    # unscoped as the master key. This holds even when the caller
    # explicitly passes `mongo_direct: false`: an explicit false cannot
    # opt a scoped query out of ACL/CLP enforcement. Promote to mongo-
    # direct, or fail closed when direct Mongo is unavailable (refuse
    # rather than leak unscoped rows).
    if mongo_ready
      use_mongo_direct = true
    else
      raise_scoped_aggregation_requires_mongo_direct!
    end
  elsif use_mongo_direct.nil?
    # Unscoped auto-routing: $inQuery/$notInQuery → $lookup pipelines and
    # SDK-injected internal-field ($rperm/_wperm) pipelines can't be served
    # by REST /aggregate, so prefer mongo-direct when available. An unscoped
    # internal-field pipeline keeps the REST fallback (a master-key
    # correctness edge, not an enforcement bypass).
    if (lookup_stages && lookup_stages.any?) || uses_internal_fields
      use_mongo_direct = true if mongo_ready
    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



3669
3670
3671
3672
3673
3674
3675
3676
3677
3678
3679
3680
3681
3682
3683
3684
3685
3686
3687
3688
3689
3690
3691
3692
3693
3694
3695
3696
3697
3698
3699
3700
3701
3702
3703
3704
3705
3706
3707
3708
3709
3710
3711
# File 'lib/parse/query.rb', line 3669

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?
  mongo_ready = defined?(Parse::MongoDB) && Parse::MongoDB.enabled?
  use_mongo_direct = mongo_direct

  if scoped
    # A scoped aggregation must never reach REST /aggregate (master-key-
    # only, unenforced) — not even when the caller explicitly passes
    # mongo_direct: false. Promote to mongo-direct, or fail closed.
    if mongo_ready
      use_mongo_direct = true
    else
      raise_scoped_aggregation_requires_mongo_direct!
    end
  elsif use_mongo_direct.nil?
    if has_lookup_stages || uses_internal_fields
      use_mongo_direct = true if mongo_ready
    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:



4162
4163
4164
4165
4166
# File 'lib/parse/query.rb', line 4162

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

#as_json(*args) ⇒ Hash

Returns:



4288
4289
4290
# File 'lib/parse/query.rb', line 4288

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:



2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
# File 'lib/parse/query.rb', line 2577

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:



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
# File 'lib/parse/query.rb', line 2633

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:



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
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
# File 'lib/parse/query.rb', line 2467

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.



4421
4422
4423
4424
4425
4426
4427
4428
4429
4430
4431
4432
4433
4434
4435
# File 'lib/parse/query.rb', line 4421

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]



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
3964
3965
3966
3967
3968
3969
3970
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
# File 'lib/parse/query.rb', line 3905

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



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
2722
2723
2724
2725
2726
2727
2728
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
# File 'lib/parse/query.rb', line 2668

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



4114
4115
4116
4117
4118
4119
4120
4121
4122
4123
4124
4125
4126
4127
4128
4129
4130
4131
4132
# File 'lib/parse/query.rb', line 4114

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



2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
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
2818
2819
2820
2821
2822
# File 'lib/parse/query.rb', line 2766

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



5600
5601
5602
5603
5604
5605
5606
5607
5608
5609
5610
5611
5612
5613
5614
5615
5616
5617
5618
5619
5620
5621
5622
5623
5624
# File 'lib/parse/query.rb', line 5600

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:



4306
4307
4308
4309
4310
4311
4312
4313
4314
4315
4316
4317
4318
4319
4320
4321
4322
4323
4324
4325
4326
4327
4328
4329
4330
4331
4332
4333
4334
4335
4336
4337
4338
4339
4340
4341
4342
4343
4344
4345
4346
4347
4348
4349
4350
4351
# File 'lib/parse/query.rb', line 4306

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.



4355
4356
4357
# File 'lib/parse/query.rb', line 4355

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.



3168
3169
3170
3171
3172
3173
3174
3175
3176
# File 'lib/parse/query.rb', line 3168

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



2860
2861
2862
2863
2864
2865
2866
2867
2868
2869
2870
2871
2872
2873
2874
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
# File 'lib/parse/query.rb', line 2860

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



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
2960
2961
2962
2963
2964
2965
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
# File 'lib/parse/query.rb', line 2912

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.



3152
3153
3154
3155
3156
3157
3158
3159
3160
# File 'lib/parse/query.rb', line 3152

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.



3096
3097
3098
3099
3100
3101
3102
3103
3104
3105
3106
3107
3108
3109
# File 'lib/parse/query.rb', line 3096

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.



3118
3119
3120
3121
3122
3123
3124
3125
3126
# File 'lib/parse/query.rb', line 3118

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.)



3183
3184
3185
3186
3187
3188
3189
3190
3191
3192
# File 'lib/parse/query.rb', line 3183

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.



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

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



3063
3064
3065
3066
3067
3068
3069
3070
3071
3072
3073
3074
3075
3076
3077
3078
3079
3080
3081
3082
3083
3084
3085
3086
3087
3088
3089
# File 'lib/parse/query.rb', line 3063

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



3003
3004
3005
3006
3007
3008
3009
3010
3011
3012
3013
3014
3015
3016
3017
3018
3019
3020
3021
3022
3023
3024
3025
3026
3027
3028
3029
3030
3031
3032
3033
3034
3035
3036
3037
3038
3039
3040
3041
3042
3043
3044
# File 'lib/parse/query.rb', line 3003

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:



2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
# File 'lib/parse/query.rb', line 2273

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:



3261
3262
3263
# File 'lib/parse/query.rb', line 3261

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:



4171
4172
4173
4174
4175
4176
4177
4178
4179
4180
# File 'lib/parse/query.rb', line 4171

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



3365
3366
3367
3368
3369
3370
3371
3372
3373
3374
3375
3376
3377
3378
3379
3380
3381
3382
3383
3384
3385
3386
3387
3388
3389
3390
# File 'lib/parse/query.rb', line 3365

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:



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
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
# File 'lib/parse/query.rb', line 2337

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:



2421
2422
2423
2424
2425
2426
# File 'lib/parse/query.rb', line 2421

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.



4692
4693
4694
4695
4696
4697
4698
4699
# File 'lib/parse/query.rb', line 4692

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)



3851
3852
3853
3854
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
# File 'lib/parse/query.rb', line 3851

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



3336
3337
3338
3339
3340
3341
3342
3343
3344
3345
3346
3347
3348
3349
3350
3351
3352
3353
3354
3355
3356
# File 'lib/parse/query.rb', line 3336

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



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
4060
4061
4062
4063
4064
4065
4066
4067
4068
4069
4070
4071
4072
4073
4074
4075
4076
4077
4078
4079
4080
4081
4082
4083
4084
4085
4086
4087
4088
4089
4090
4091
4092
4093
4094
4095
4096
4097
4098
4099
4100
4101
4102
4103
4104
4105
4106
4107
4108
4109
# File 'lib/parse/query.rb', line 4021

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:



2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
# File 'lib/parse/query.rb', line 2223

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



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
# File 'lib/parse/query.rb', line 2830

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:



4504
4505
4506
4507
4508
4509
4510
4511
4512
4513
4514
# File 'lib/parse/query.rb', line 4504

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:



4664
4665
4666
4667
4668
4669
4670
4671
4672
4673
4674
4675
4676
4677
4678
# File 'lib/parse/query.rb', line 4664

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.



4538
4539
4540
4541
4542
4543
4544
4545
4546
4547
4548
4549
4550
4551
4552
4553
4554
4555
4556
4557
4558
4559
4560
4561
4562
4563
4564
4565
4566
4567
4568
4569
4570
4571
4572
4573
4574
4575
4576
4577
4578
4579
4580
# File 'lib/parse/query.rb', line 4538

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



4137
4138
4139
4140
4141
4142
4143
4144
4145
4146
4147
4148
4149
4150
4151
# File 'lib/parse/query.rb', line 4137

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.



4461
4462
4463
4464
4465
4466
4467
4468
4469
4470
4471
4472
4473
4474
4475
# File 'lib/parse/query.rb', line 4461

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.



4442
4443
4444
4445
4446
4447
4448
4449
4450
4451
4452
4453
4454
4455
4456
# File 'lib/parse/query.rb', line 4442

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:



5837
5838
5839
5840
5841
# File 'lib/parse/query.rb', line 5837

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:



5850
5851
5852
5853
5854
# File 'lib/parse/query.rb', line 5850

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:



5809
5810
5811
5812
5813
# File 'lib/parse/query.rb', line 5809

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:



5824
5825
5826
5827
5828
# File 'lib/parse/query.rb', line 5824

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



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

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



3896
3897
3898
3899
3900
# File 'lib/parse/query.rb', line 3896

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.



4296
4297
4298
# File 'lib/parse/query.rb', line 4296

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

#prettyString

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

Returns:



4395
4396
4397
# File 'lib/parse/query.rb', line 4395

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:



5791
5792
5793
5794
# File 'lib/parse/query.rb', line 5791

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:



5763
5764
5765
# File 'lib/parse/query.rb', line 5763

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:



5777
5778
5779
# File 'lib/parse/query.rb', line 5777

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:



5739
5740
5741
# File 'lib/parse/query.rb', line 5739

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:



5751
5752
5753
# File 'lib/parse/query.rb', line 5751

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



2102
2103
2104
# File 'lib/parse/query.rb', line 2102

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:



5659
5660
5661
5662
5663
# File 'lib/parse/query.rb', line 5659

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:



5676
5677
5678
5679
5680
# File 'lib/parse/query.rb', line 5676

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



4389
4390
4391
# File 'lib/parse/query.rb', line 4389

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



2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
# File 'lib/parse/query.rb', line 2083

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:



2110
2111
2112
# File 'lib/parse/query.rb', line 2110

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:



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
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
# File 'lib/parse/query.rb', line 2139

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)



3209
3210
3211
3212
3213
3214
3215
3216
3217
3218
3219
3220
3221
3222
3223
3224
3225
3226
3227
3228
3229
3230
3231
3232
3233
# File 'lib/parse/query.rb', line 3209

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:



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

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.



4402
4403
4404
4405
4406
4407
4408
4409
4410
4411
4412
4413
4414
4415
4416
# File 'lib/parse/query.rb', line 4402

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:



4250
4251
4252
4253
4254
4255
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
# File 'lib/parse/query.rb', line 4250

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



4617
4618
4619
4620
4621
4622
4623
4624
4625
4626
4627
4628
4629
4630
4631
4632
4633
4634
4635
4636
4637
4638
4639
4640
4641
4642
4643
4644
4645
# File 'lib/parse/query.rb', line 4617

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



3616
3617
3618
3619
# File 'lib/parse/query.rb', line 3616

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.



3647
3648
3649
3650
3651
# File 'lib/parse/query.rb', line 3647

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.



3637
3638
3639
3640
3641
# File 'lib/parse/query.rb', line 3637

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:



5704
5705
5706
5707
5708
# File 'lib/parse/query.rb', line 5704

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:



5721
5722
5723
5724
5725
# File 'lib/parse/query.rb', line 5721

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