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 retrieve 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 # => {"fieldOne"=>1, "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 # => {"field_one"=>1, "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 # => {"FieldOne"=>1, "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:



1965
1966
1967
# File 'lib/parse/query.rb', line 1965

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:



1961
1962
1963
# File 'lib/parse/query.rb', line 1961

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



5650
5651
5652
5653
5654
5655
5656
5657
5658
5659
5660
5661
5662
5663
5664
5665
5666
5667
5668
5669
5670
5671
5672
5673
5674
5675
5676
5677
5678
5679
5680
5681
# File 'lib/parse/query.rb', line 5650

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



5615
5616
5617
5618
5619
5620
5621
5622
5623
5624
5625
5626
5627
5628
5629
5630
5631
5632
5633
5634
5635
5636
5637
5638
5639
5640
5641
5642
5643
# File 'lib/parse/query.rb', line 5615

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:



959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
# File 'lib/parse/query.rb', line 959

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)


937
938
939
940
941
# File 'lib/parse/query.rb', line 937

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



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
3609
3610
3611
3612
3613
3614
3615
3616
3617
3618
3619
3620
3621
3622
3623
3624
3625
3626
3627
3628
3629
3630
3631
3632
3633
3634
3635
3636
3637
3638
3639
3640
3641
3642
3643
3644
3645
3646
3647
3648
3649
3650
3651
3652
3653
3654
3655
3656
3657
3658
3659
3660
3661
3662
3663
3664
3665
3666
3667
3668
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
# File 'lib/parse/query.rb', line 3505

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



3758
3759
3760
3761
3762
3763
3764
3765
3766
3767
3768
3769
3770
3771
3772
3773
3774
3775
3776
3777
3778
3779
3780
3781
3782
3783
3784
3785
3786
3787
3788
3789
3790
3791
3792
3793
3794
3795
3796
3797
3798
3799
3800
# File 'lib/parse/query.rb', line 3758

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:



4251
4252
4253
4254
4255
# File 'lib/parse/query.rb', line 4251

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

#as_json(*args) ⇒ Hash

Returns:



4377
4378
4379
# File 'lib/parse/query.rb', line 4377

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:



2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
# File 'lib/parse/query.rb', line 2660

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, converted to Mongo storage form
  # first (see #atlas_search for the reasoning — a raw Parse::Pointer
  # fails BSON serialization and un-prefixed pointer fields target the
  # wrong column).
  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)
      options[:filter] = (options[:filter] || {}).merge(mongo_constraints)
    end
  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:



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

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:



2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
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
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
# File 'lib/parse/query.rb', line 2532

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: the caller configures the $search stage
    # directly via the yielded SearchBuilder. Route through
    # Parse::AtlasSearch.search_with_stage so the SAME SDK-side
    # enforcement chain as the options API runs — scope resolution,
    # CLP `find`, the ACL $match placed AFTER $search (not prepended
    # to stage 0), protectedFields/pointerFields redaction, and the
    # $expr protected-field oracle guard on the caller filter.
    #
    # The previous implementation called Parse::MongoDB.aggregate
    # with `allow_internal_fields: true` and NO auth forwarding: it
    # both ran unscoped (resolved to :public / master, no ACL) and,
    # because that helper prepends the ACL $match at position 0, was
    # rejected outright by Atlas (which requires $search at stage 0).
    # It was non-functional under both supported security configs.
    index_name = options[:index] || Parse::AtlasSearch.default_index
    builder = Parse::AtlasSearch::SearchBuilder.new(index_name: index_name)
    yield builder

    # Carry the query's existing constraints as the caller filter,
    # interpolated into a post-$search $match. The constraints MUST be
    # converted to Mongo storage form first: `compile_where` yields the
    # Parse REST shape (pointer keys like `owner`, values that are live
    # Parse::Pointer objects / Parse date dicts). Fed to a `$match`
    # verbatim, a Parse::Pointer fails BSON serialization and dates/ids
    # target the wrong field (`owner` vs `_p_owner`). `convert_-
    # constraints_for_direct_mongodb` rewrites keys/values to storage
    # form — the same conversion the mongo-direct path applies, and the
    # one the pre-5.5.5 block path used before this branch was rewritten.
    filter = options[:filter]
    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)
        filter = (filter || {}).merge(mongo_constraints)
      end
    end

    search_opts = {
      filter: filter,
      limit: limit,
      skip: skip_val,
      class_name: @table,
      raw: options[:raw],
      **atlas_search_auth_kwargs(options),
    }
    rp = options.key?(:read_preference) ? options[:read_preference] : @read_preference
    search_opts[:read_preference] = rp unless rp.nil?
    search_opts[:max_time_ms] = options[:max_time_ms] if options.key?(:max_time_ms)

    Parse::AtlasSearch.search_with_stage(@table, builder.build, **search_opts)
  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. Convert to Mongo storage form
    # first (same reasoning as the block branch above): compile_where
    # yields the Parse REST shape (live Parse::Pointer objects, Parse
    # date dicts, un-prefixed pointer field names) which a raw $match
    # cannot serialize / would target the wrong column.
    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)
        options[:filter] = (options[:filter] || {}).merge(mongo_constraints)
      end
    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.



4510
4511
4512
4513
4514
4515
4516
4517
4518
4519
4520
4521
4522
4523
4524
# File 'lib/parse/query.rb', line 4510

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]



3994
3995
3996
3997
3998
3999
4000
4001
4002
4003
4004
4005
4006
4007
4008
4009
4010
4011
4012
4013
4014
4015
4016
4017
4018
4019
4020
4021
4022
4023
4024
4025
4026
4027
4028
4029
4030
4031
4032
4033
4034
4035
4036
4037
4038
4039
4040
4041
4042
4043
4044
4045
4046
4047
4048
4049
4050
4051
4052
4053
4054
4055
4056
4057
4058
4059
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
# File 'lib/parse/query.rb', line 3994

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



2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
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
2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843
2844
2845
2846
2847
# File 'lib/parse/query.rb', line 2757

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



4203
4204
4205
4206
4207
4208
4209
4210
4211
4212
4213
4214
4215
4216
4217
4218
4219
4220
4221
# File 'lib/parse/query.rb', line 4203

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



2855
2856
2857
2858
2859
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
2907
2908
2909
2910
2911
# File 'lib/parse/query.rb', line 2855

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.



620
621
622
623
# File 'lib/parse/query.rb', line 620

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



5689
5690
5691
5692
5693
5694
5695
5696
5697
5698
5699
5700
5701
5702
5703
5704
5705
5706
5707
5708
5709
5710
5711
5712
5713
# File 'lib/parse/query.rb', line 5689

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:



4395
4396
4397
4398
4399
4400
4401
4402
4403
4404
4405
4406
4407
4408
4409
4410
4411
4412
4413
4414
4415
4416
4417
4418
4419
4420
4421
4422
4423
4424
4425
4426
4427
4428
4429
4430
4431
4432
4433
4434
4435
4436
4437
4438
4439
4440
# File 'lib/parse/query.rb', line 4395

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.



4444
4445
4446
# File 'lib/parse/query.rb', line 4444

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
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
# 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

    # SEC-06: privilege/auth control options must NOT be settable from an
    # untrusted, string-keyed conditions hash. `use_master_key` via a
    # forwarded params hash (e.g. `Query.new(Cls, {"use_master_key" =>
    # true, "ownerId" => "victim"})`) is an ACL/CLP-bypass mass-
    # assignment; `session` swaps the query's auth principal. Honor these
    # two ONLY when the caller passed a Symbol key (code-authored). A
    # string form is treated as an ordinary field constraint — fail-safe:
    # it matches a literal field of that name and never elevates.
    if !expression.is_a?(Symbol) && (expr_sym == :use_master_key || expr_sym == :session)
      warn "[Parse::Query] Ignoring string-keyed control option " \
           "#{expression.inspect}; auth/privilege options (:use_master_key, " \
           ":session) are honored only as symbol keys to prevent " \
           "mass-assignment from untrusted params. Treating it as a field " \
           "constraint. Set it via the query method (e.g. `use_master_key: " \
           "true` as a symbol) if you intended to control authentication."
      add_constraint(expression, value)
      next
    end

    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.



1030
1031
1032
# File 'lib/parse/query.rb', line 1030

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.



3257
3258
3259
3260
3261
3262
3263
3264
3265
# File 'lib/parse/query.rb', line 3257

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



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

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



3001
3002
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
3045
3046
3047
3048
3049
3050
3051
3052
3053
3054
3055
3056
3057
3058
3059
3060
3061
3062
3063
3064
3065
3066
3067
3068
3069
3070
3071
3072
3073
3074
3075
3076
3077
3078
3079
3080
3081
3082
3083
3084
3085
# File 'lib/parse/query.rb', line 3001

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.



3241
3242
3243
3244
3245
3246
3247
3248
3249
# File 'lib/parse/query.rb', line 3241

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.



3185
3186
3187
3188
3189
3190
3191
3192
3193
3194
3195
3196
3197
3198
# File 'lib/parse/query.rb', line 3185

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.



3207
3208
3209
3210
3211
3212
3213
3214
3215
# File 'lib/parse/query.rb', line 3207

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



3272
3273
3274
3275
3276
3277
3278
3279
3280
3281
# File 'lib/parse/query.rb', line 3272

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.



3219
3220
3221
3222
3223
3224
3225
3226
3227
3228
# File 'lib/parse/query.rb', line 3219

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



3152
3153
3154
3155
3156
3157
3158
3159
3160
3161
3162
3163
3164
3165
3166
3167
3168
3169
3170
3171
3172
3173
3174
3175
3176
3177
3178
# File 'lib/parse/query.rb', line 3152

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



3092
3093
3094
3095
3096
3097
3098
3099
3100
3101
3102
3103
3104
3105
3106
3107
3108
3109
3110
3111
3112
3113
3114
3115
3116
3117
3118
3119
3120
3121
3122
3123
3124
3125
3126
3127
3128
3129
3130
3131
3132
3133
# File 'lib/parse/query.rb', line 3092

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



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
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
# File 'lib/parse/query.rb', line 1257

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:



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

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



1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
# File 'lib/parse/query.rb', line 1333

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:



3350
3351
3352
# File 'lib/parse/query.rb', line 3350

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:



4260
4261
4262
4263
4264
4265
4266
4267
4268
4269
# File 'lib/parse/query.rb', line 4260

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



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

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



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
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
# File 'lib/parse/query.rb', line 1135

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:



2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
# File 'lib/parse/query.rb', line 2402

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:



2486
2487
2488
2489
2490
2491
# File 'lib/parse/query.rb', line 2486

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.



4781
4782
4783
4784
4785
4786
4787
4788
# File 'lib/parse/query.rb', line 4781

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



1228
1229
1230
# File 'lib/parse/query.rb', line 1228

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. Also detects the fiber-local ambient session set by Parse.with_session so that aggregations inside a with_session block are treated as scoped — consistent with how Parse::Client#request already scopes REST calls.

Returns:

  • (Boolean)


1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
# File 'lib/parse/query.rb', line 1839

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
  # An ambient Parse.with_session counts as scope ONLY when the query did
  # not explicitly request master-key mode — mirroring Parse::Client#request,
  # where an explicit use_master_key: true is a deliberate admin call that
  # skips the ambient session. Otherwise an admin aggregation inside a
  # with_session block would be wrongly forced to mongo-direct / fail-closed.
  unless use_master_key == true
    ambient = ambient_session_token
    return true if ambient.is_a?(String) && !ambient.empty?
  end
  false
end

#each { ... } ⇒ Array

Yields:

  • a block yield for each object in the result

Returns:

See Also:

  • Array#each


1371
1372
1373
1374
# File 'lib/parse/query.rb', line 1371

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)


685
686
687
688
689
690
691
692
693
694
695
# File 'lib/parse/query.rb', line 685

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)



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

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



3425
3426
3427
3428
3429
3430
3431
3432
3433
3434
3435
3436
3437
3438
3439
3440
3441
3442
3443
3444
3445
# File 'lib/parse/query.rb', line 3425

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



4110
4111
4112
4113
4114
4115
4116
4117
4118
4119
4120
4121
4122
4123
4124
4125
4126
4127
4128
4129
4130
4131
4132
4133
4134
4135
4136
4137
4138
4139
4140
4141
4142
4143
4144
4145
4146
4147
4148
4149
4150
4151
4152
4153
4154
4155
4156
4157
4158
4159
4160
4161
4162
4163
4164
4165
4166
4167
4168
4169
4170
4171
4172
4173
4174
4175
4176
4177
4178
4179
4180
4181
4182
4183
4184
4185
4186
4187
4188
4189
4190
4191
4192
4193
4194
4195
4196
4197
4198
# File 'lib/parse/query.rb', line 4110

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:



1710
1711
1712
1713
1714
1715
1716
# File 'lib/parse/query.rb', line 1710

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.



1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
# File 'lib/parse/query.rb', line 1407

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:



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
2318
2319
2320
2321
2322
# File 'lib/parse/query.rb', line 2288

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:



1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
# File 'lib/parse/query.rb', line 1494

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



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

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:



4593
4594
4595
4596
4597
4598
4599
4600
4601
4602
4603
# File 'lib/parse/query.rb', line 4593

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:



4753
4754
4755
4756
4757
4758
4759
4760
4761
4762
4763
4764
4765
4766
4767
# File 'lib/parse/query.rb', line 4753

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.



4627
4628
4629
4630
4631
4632
4633
4634
4635
4636
4637
4638
4639
4640
4641
4642
4643
4644
4645
4646
4647
4648
4649
4650
4651
4652
4653
4654
4655
4656
4657
4658
4659
4660
4661
4662
4663
4664
4665
4666
4667
4668
4669
# File 'lib/parse/query.rb', line 4627

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



4226
4227
4228
4229
4230
4231
4232
4233
4234
4235
4236
4237
4238
4239
4240
# File 'lib/parse/query.rb', line 4226

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



930
931
932
# File 'lib/parse/query.rb', line 930

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)


917
918
919
920
921
922
923
924
925
926
927
# File 'lib/parse/query.rb', line 917

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)


639
640
641
642
643
644
645
646
647
648
649
# File 'lib/parse/query.rb', line 639

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:



1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
# File 'lib/parse/query.rb', line 1478

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:



1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
# File 'lib/parse/query.rb', line 1456

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)


835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
# File 'lib/parse/query.rb', line 835

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


1379
1380
1381
1382
# File 'lib/parse/query.rb', line 1379

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.



4550
4551
4552
4553
4554
4555
4556
4557
4558
4559
4560
4561
4562
4563
4564
# File 'lib/parse/query.rb', line 4550

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.



4531
4532
4533
4534
4535
4536
4537
4538
4539
4540
4541
4542
4543
4544
4545
# File 'lib/parse/query.rb', line 4531

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:



5926
5927
5928
5929
5930
# File 'lib/parse/query.rb', line 5926

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:



5939
5940
5941
5942
5943
# File 'lib/parse/query.rb', line 5939

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:



5898
5899
5900
5901
5902
# File 'lib/parse/query.rb', line 5898

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:



5913
5914
5915
5916
5917
# File 'lib/parse/query.rb', line 5913

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.



1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
# File 'lib/parse/query.rb', line 1080

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)


756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
# File 'lib/parse/query.rb', line 756

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



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

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



3985
3986
3987
3988
3989
# File 'lib/parse/query.rb', line 3985

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.



714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
# File 'lib/parse/query.rb', line 714

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.



4385
4386
4387
# File 'lib/parse/query.rb', line 4385

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

#prettyString

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

Returns:



4484
4485
4486
# File 'lib/parse/query.rb', line 4484

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:



5880
5881
5882
5883
# File 'lib/parse/query.rb', line 5880

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:



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

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:



5866
5867
5868
# File 'lib/parse/query.rb', line 5866

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:



5828
5829
5830
# File 'lib/parse/query.rb', line 5828

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:



5840
5841
5842
# File 'lib/parse/query.rb', line 5840

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.



1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
# File 'lib/parse/query.rb', line 1863

def raise_scoped_aggregation_requires_mongo_direct!
  raise MongoDirectRequired,
    "[Parse::Query] This scoped aggregation (session_token / " \
    "scope_to_user / scope_to_role, or an active Parse.with_session " \
    "block) 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



2167
2168
2169
# File 'lib/parse/query.rb', line 2167

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)


868
869
870
871
# File 'lib/parse/query.rb', line 868

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:



5748
5749
5750
5751
5752
# File 'lib/parse/query.rb', line 5748

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:



5765
5766
5767
5768
5769
# File 'lib/parse/query.rb', line 5765

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)


898
899
900
901
902
# File 'lib/parse/query.rb', line 898

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



4478
4479
4480
# File 'lib/parse/query.rb', line 4478

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



2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
# File 'lib/parse/query.rb', line 2148

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)


1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
# File 'lib/parse/query.rb', line 1819

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:



2175
2176
2177
# File 'lib/parse/query.rb', line 2175

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.



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
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
# File 'lib/parse/query.rb', line 1743

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:



2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
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
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
# File 'lib/parse/query.rb', line 2204

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)



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

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)


1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
# File 'lib/parse/query.rb', line 1947

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)


1912
1913
1914
1915
1916
1917
# File 'lib/parse/query.rb', line 1912

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


1387
1388
1389
1390
# File 'lib/parse/query.rb', line 1387

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)


797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
# File 'lib/parse/query.rb', line 797

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:



3393
3394
3395
3396
3397
3398
3399
3400
3401
3402
3403
3404
3405
3406
3407
# File 'lib/parse/query.rb', line 3393

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.



4491
4492
4493
4494
4495
4496
4497
4498
4499
4500
4501
4502
4503
4504
4505
# File 'lib/parse/query.rb', line 4491

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


1394
1395
1396
# File 'lib/parse/query.rb', line 1394

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:



4339
4340
4341
4342
4343
4344
4345
4346
4347
4348
4349
4350
4351
4352
4353
4354
4355
4356
4357
4358
4359
4360
4361
4362
4363
4364
4365
4366
4367
4368
4369
4370
4371
4372
4373
4374
# File 'lib/parse/query.rb', line 4339

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



4706
4707
4708
4709
4710
4711
4712
4713
4714
4715
4716
4717
4718
4719
4720
4721
4722
4723
4724
4725
4726
4727
4728
4729
4730
4731
4732
4733
4734
# File 'lib/parse/query.rb', line 4706

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



3705
3706
3707
3708
# File 'lib/parse/query.rb', line 3705

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.



3736
3737
3738
3739
3740
# File 'lib/parse/query.rb', line 3736

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.



3726
3727
3728
3729
3730
# File 'lib/parse/query.rb', line 3726

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:

  • expressions (Hash) (defaults to: nil)

    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:



1055
1056
1057
1058
1059
1060
1061
1062
# File 'lib/parse/query.rb', line 1055

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



1037
1038
1039
# File 'lib/parse/query.rb', line 1037

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:



5793
5794
5795
5796
5797
# File 'lib/parse/query.rb', line 5793

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:



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

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:



1110
1111
1112
1113
1114
1115
# File 'lib/parse/query.rb', line 1110

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