Class: Parse::Query

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

Overview

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

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

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

Field-Formatter

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

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

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

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

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

Defined Under Namespace

Classes: MongoDirectRequired, PointerShapeError

Constant Summary collapse

BUILT_IN_PARSE_CLASSES =

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

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

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

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

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

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

Examples:

Force a specific index

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

Returns:

:_hint_unset_
RESERVED_EXCLUDE_KEYS =

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

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

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

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

Examples:

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

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

Returns:

  • (Aggregation)

    an aggregation object that can be executed

Parse::PipelineSecurity::DENIED_OPERATORS

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

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

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

Overloads:

  • #new(table) ⇒ Query

    Create a query for this Parse collection name.

    Examples:

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

    Parameters:

    • table (String)

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

    • constraints (Hash)

      a set of query constraints.

  • #new(parseSubclass) ⇒ Query

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

    Examples:

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

    Parameters:

    • parseSubclass (Parse::Object)

      the Parse model constant

    • constraints (Hash)

      a set of query constraints.

Raises:

  • (ArgumentError)


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

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

Class Attribute Details

.allow_scope_introspectionSymbol

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

Returns:



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

.field_formatterSymbol

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

Returns:



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

def field_formatter
  @field_formatter
end

Instance Attribute Details

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

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

Returns:



1896
1897
1898
# File 'lib/parse/query.rb', line 1896

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:



1892
1893
1894
# File 'lib/parse/query.rb', line 1892

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



5388
5389
5390
5391
5392
5393
5394
5395
5396
5397
5398
5399
5400
5401
5402
5403
5404
5405
5406
5407
5408
5409
5410
5411
5412
5413
5414
5415
5416
5417
5418
5419
# File 'lib/parse/query.rb', line 5388

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



5353
5354
5355
5356
5357
5358
5359
5360
5361
5362
5363
5364
5365
5366
5367
5368
5369
5370
5371
5372
5373
5374
5375
5376
5377
5378
5379
5380
5381
# File 'lib/parse/query.rb', line 5353

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

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

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

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

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

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

  result
end

.parse_keys_to_nested_keys(keys) ⇒ Hash

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

Parameters:

  • keys (Array<Symbol, String>)

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

Returns:

  • (Hash)

    a map of nested field names to their fetched keys



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

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

  nested_map = {}

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

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

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

  nested_map
end

.pointer_shape_warnedObject

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



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

def pointer_shape_warned
  @pointer_shape_warned ||= {}
end

.reset_known_parse_classes!Object

Allow resetting the cached known classes (useful for testing)



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

def self.reset_known_parse_classes!
  @known_parse_classes = nil
end

.to_snake_case(str) ⇒ String

Convert camelCase string to snake_case

Parameters:

  • str (String)

    the camelCase string

Returns:

  • (String)

    the snake_case string



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

def to_snake_case(str)
  str.to_s.underscore
end

Instance Method Details

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

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

Examples:

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

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

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

Parameters:

  • operator (Parse::Operator)

    an operator object containing the operation and operand.

  • value (Object) (defaults to: nil)

    the value for the constraint.

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

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

Returns:

  • (self)

See Also:



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

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

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

#add_constraints(list) ⇒ self

Combine a list of Constraint objects

Parameters:

Returns:

  • (self)


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

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

#after_prepare { ... } ⇒ Object

A callback called after the query is compiled

Yields:

  • A block to execute for the callback.

See Also:

  • ActiveModel::Callbacks


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

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

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



3342
3343
3344
3345
3346
3347
3348
3349
3350
3351
3352
3353
3354
3355
3356
3357
3358
3359
3360
3361
3362
3363
3364
3365
3366
3367
3368
3369
3370
3371
3372
3373
3374
3375
3376
3377
3378
3379
3380
3381
3382
3383
3384
3385
3386
3387
3388
3389
3390
3391
3392
3393
3394
3395
3396
3397
3398
3399
3400
3401
3402
3403
3404
3405
3406
3407
3408
3409
3410
3411
3412
3413
3414
3415
3416
3417
3418
3419
3420
3421
3422
3423
3424
3425
3426
3427
3428
3429
3430
3431
3432
3433
3434
3435
3436
3437
3438
3439
3440
3441
3442
3443
3444
3445
3446
3447
3448
3449
3450
3451
3452
3453
3454
3455
3456
3457
3458
3459
3460
3461
3462
3463
3464
3465
3466
3467
3468
3469
3470
3471
3472
3473
3474
3475
3476
3477
3478
3479
3480
3481
3482
3483
3484
3485
3486
3487
3488
# File 'lib/parse/query.rb', line 3342

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

  # Auto-detect if mongo_direct is needed (when $inQuery constraints are present and MongoDB is available)
  use_mongo_direct = mongo_direct
  if use_mongo_direct.nil? && lookup_stages && lookup_stages.any? && defined?(Parse::MongoDB) && Parse::MongoDB.enabled?
    use_mongo_direct = true
  end

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

  # 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,
                  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



3549
3550
3551
3552
3553
3554
3555
3556
3557
3558
3559
3560
3561
3562
3563
3564
# File 'lib/parse/query.rb', line 3549

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

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

  # Auto-detect if mongo_direct is needed (when $inQuery constraints are present and MongoDB is available)
  use_mongo_direct = mongo_direct
  if use_mongo_direct.nil? && has_lookup_stages && defined?(Parse::MongoDB) && Parse::MongoDB.enabled?
    use_mongo_direct = true
  end

  # Create Aggregation directly to avoid double-applying constraints
  Aggregation.new(self, pipeline, verbose: verbose, mongo_direct: use_mongo_direct || false)
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:



3989
3990
3991
3992
3993
# File 'lib/parse/query.rb', line 3989

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

#as_json(*args) ⇒ Hash

Returns:



4115
4116
4117
# File 'lib/parse/query.rb', line 4115

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:



2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
# File 'lib/parse/query.rb', line 2503

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

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

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

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

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

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

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

Examples:

Faceted search by genre and decade

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

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

Parameters:

  • query (String, nil)

    the search query text (nil for match-all)

  • facets (Hash)

    facet definitions with the following structure:

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

    search options (same as atlas_search)

Returns:

Raises:

See Also:



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

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:



2393
2394
2395
2396
2397
2398
2399
2400
2401
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
# File 'lib/parse/query.rb', line 2393

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Calculate the average of values for a specific field.

Parameters:

Returns:

  • (Float)

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



4248
4249
4250
4251
4252
4253
4254
4255
4256
4257
4258
4259
4260
4261
4262
# File 'lib/parse/query.rb', line 4248

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]



3732
3733
3734
3735
3736
3737
3738
3739
3740
3741
3742
3743
3744
3745
3746
3747
3748
3749
3750
3751
3752
3753
3754
3755
3756
3757
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
3801
3802
3803
3804
3805
3806
3807
3808
3809
3810
3811
3812
3813
3814
3815
3816
3817
3818
3819
3820
3821
3822
3823
3824
3825
3826
3827
3828
3829
3830
3831
3832
3833
3834
3835
3836
3837
3838
3839
3840
# File 'lib/parse/query.rb', line 3732

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



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
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
# File 'lib/parse/query.rb', line 2594

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



3941
3942
3943
3944
3945
3946
3947
3948
3949
3950
3951
3952
3953
3954
3955
3956
3957
3958
3959
# File 'lib/parse/query.rb', line 3941

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



2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
# File 'lib/parse/query.rb', line 2692

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

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

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

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

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

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

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

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

  stages
end

#clause(clause_name = :where) ⇒ Object

returns the query clause for the particular clause

Parameters:

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

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

Returns:

  • (Object)

    the content of the clause for this query.



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

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

#clear(item = :results) ⇒ self

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

Parameters:

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

    the clause to clear.

Returns:

  • (self)


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

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

#cloneParse::Query

Note:

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

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

Returns:

  • (Parse::Query)

    a new query object with the same constraints



5427
5428
5429
5430
5431
5432
5433
5434
5435
5436
5437
5438
5439
5440
5441
5442
5443
5444
5445
5446
5447
5448
5449
5450
5451
# File 'lib/parse/query.rb', line 5427

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:



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

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.



4182
4183
4184
# File 'lib/parse/query.rb', line 4182

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

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

Add a set of query expressions and constraints.

Examples:

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

Parameters:

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

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

Returns:

  • (self)


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

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

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

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

Parameters:

  • raw (Boolean) (defaults to: false)

    whether to return the hash form of the constraints.

Returns:

  • (Array<Parse::Constraint>)

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

  • (Hash)

    if raw is true, a hash representing the constraints.



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

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

#convert_addfields_for_direct_mongodb(spec) ⇒ Object

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

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



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

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



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

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



2838
2839
2840
2841
2842
2843
2844
2845
2846
2847
2848
2849
2850
2851
2852
2853
2854
2855
2856
2857
2858
2859
2860
2861
2862
2863
2864
2865
2866
2867
2868
2869
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
2912
2913
2914
2915
2916
2917
2918
2919
2920
2921
2922
# File 'lib/parse/query.rb', line 2838

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.



3078
3079
3080
3081
3082
3083
3084
3085
3086
# File 'lib/parse/query.rb', line 3078

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.



3022
3023
3024
3025
3026
3027
3028
3029
3030
3031
3032
3033
3034
3035
# File 'lib/parse/query.rb', line 3022

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.



3044
3045
3046
3047
3048
3049
3050
3051
3052
# File 'lib/parse/query.rb', line 3044

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



3109
3110
3111
3112
3113
3114
3115
3116
3117
3118
# File 'lib/parse/query.rb', line 3109

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.



3056
3057
3058
3059
3060
3061
3062
3063
3064
3065
# File 'lib/parse/query.rb', line 3056

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



2989
2990
2991
2992
2993
2994
2995
2996
2997
2998
2999
3000
3001
3002
3003
3004
3005
3006
3007
3008
3009
3010
3011
3012
3013
3014
3015
# File 'lib/parse/query.rb', line 2989

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



2929
2930
2931
2932
2933
2934
2935
2936
2937
2938
2939
2940
2941
2942
2943
2944
2945
2946
2947
2948
2949
2950
2951
2952
2953
2954
2955
2956
2957
2958
2959
2960
2961
2962
2963
2964
2965
2966
2967
2968
2969
2970
# File 'lib/parse/query.rb', line 2929

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

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

#count(mongo_direct: false) ⇒ Integer

Perform a count query.

Examples:

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

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

Parameters:

  • mongo_direct (Boolean) (defaults to: false)

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

Returns:

  • (Integer)

    the count result



1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
# File 'lib/parse/query.rb', line 1238

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

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

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

    # Auto-detect if MongoDB direct is needed
    use_mongo_direct = false
    if has_lookup_stages && defined?(Parse::MongoDB) && Parse::MongoDB.enabled?
      use_mongo_direct = true
    end

    # Execute aggregation
    aggregation = Aggregation.new(self, pipeline, verbose: @verbose_aggregate, mongo_direct: use_mongo_direct)
    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:



2199
2200
2201
2202
2203
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
# File 'lib/parse/query.rb', line 2199

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



1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
# File 'lib/parse/query.rb', line 1296

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:



3187
3188
3189
# File 'lib/parse/query.rb', line 3187

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:



3998
3999
4000
4001
4002
4003
4004
4005
4006
4007
# File 'lib/parse/query.rb', line 3998

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



3291
3292
3293
3294
3295
3296
3297
3298
3299
3300
3301
3302
3303
3304
3305
3306
3307
3308
3309
3310
3311
3312
3313
3314
3315
3316
# File 'lib/parse/query.rb', line 3291

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

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

      # Skip if identical
      next if prev_match == curr_match

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

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

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

Note:

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

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

Examples:

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

Parameters:

  • field (Symbol|String)

    The name of the field used for filtering.

  • mongo_direct (Boolean) (defaults to: false)

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

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

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

Version:

  • 1.8.0



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

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

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

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

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

  sort_dir = distinct_sort_direction(order)

  # Format field for aggregation
  formatted_field = format_aggregation_field(field)

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

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

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

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

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

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

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

Note:

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

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

Examples:

Basic usage

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

With pointer fields

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

Parameters:

  • field (Symbol, String)

    the field name to get distinct values for

  • return_pointers (Boolean) (defaults to: false)

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

Returns:

  • (Array)

    array of distinct values

Raises:

See Also:



2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
# File 'lib/parse/query.rb', line 2263

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:



2347
2348
2349
2350
2351
2352
# File 'lib/parse/query.rb', line 2347

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.



4519
4520
4521
4522
4523
4524
4525
4526
# File 'lib/parse/query.rb', line 4519

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

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

#distinct_pointers(field, order: nil) ⇒ Array

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

Parameters:

  • field (Symbol, String)

    the field name to get distinct values for

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

    forwarded to #distinct.

Returns:

  • (Array)

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



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

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

#distinct_query_is_scoped?Boolean

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

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

Returns:

  • (Boolean)


1799
1800
1801
1802
1803
1804
# File 'lib/parse/query.rb', line 1799

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

#each { ... } ⇒ Array

Yields:

  • a block yield for each object in the result

Returns:

See Also:

  • Array#each


1334
1335
1336
1337
# File 'lib/parse/query.rb', line 1334

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

#exclude_keys(*fields) ⇒ self

Note:

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

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

Examples:

Omit a single sensitive field

Post.query.exclude_keys(:secret_token).results

Omit multiple fields

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

Parameters:

Returns:

  • (self)


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

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

#execute_aggregation_pipelineAggregation

Execute an aggregation pipeline for queries with pipeline constraints

Returns:

  • (Aggregation)

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



3694
3695
3696
3697
3698
3699
3700
3701
3702
3703
3704
3705
3706
3707
3708
3709
3710
3711
3712
3713
3714
3715
3716
3717
3718
# File 'lib/parse/query.rb', line 3694

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

  # 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
    if has_lookup_stages || pipeline_uses_internal_fields?(pipeline)
      use_mongo_direct = true
    end
  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)
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



3262
3263
3264
3265
3266
3267
3268
3269
3270
3271
3272
3273
3274
3275
3276
3277
3278
3279
3280
3281
3282
# File 'lib/parse/query.rb', line 3262

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



3848
3849
3850
3851
3852
3853
3854
3855
3856
3857
3858
3859
3860
3861
3862
3863
3864
3865
3866
3867
3868
3869
3870
3871
3872
3873
3874
3875
3876
3877
3878
3879
3880
3881
3882
3883
3884
3885
3886
3887
3888
3889
3890
3891
3892
3893
3894
3895
3896
3897
3898
3899
3900
3901
3902
3903
3904
3905
3906
3907
3908
3909
3910
3911
3912
3913
3914
3915
3916
3917
3918
3919
3920
3921
3922
3923
3924
3925
3926
3927
3928
3929
3930
3931
3932
3933
3934
3935
3936
# File 'lib/parse/query.rb', line 3848

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:



1673
1674
1675
1676
1677
1678
1679
# File 'lib/parse/query.rb', line 1673

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.



1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
# File 'lib/parse/query.rb', line 1370

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:



2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
# File 'lib/parse/query.rb', line 2149

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:



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

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



2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
# File 'lib/parse/query.rb', line 2756

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:



4331
4332
4333
4334
4335
4336
4337
4338
4339
4340
4341
# File 'lib/parse/query.rb', line 4331

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:



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

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.



4365
4366
4367
4368
4369
4370
4371
4372
4373
4374
4375
4376
4377
4378
4379
4380
4381
4382
4383
4384
4385
4386
4387
4388
4389
4390
4391
4392
4393
4394
4395
4396
4397
4398
4399
4400
4401
4402
4403
4404
4405
4406
4407
# File 'lib/parse/query.rb', line 4365

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



3964
3965
3966
3967
3968
3969
3970
3971
3972
3973
3974
3975
3976
3977
3978
# File 'lib/parse/query.rb', line 3964

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

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

#include(*fields) ⇒ Object

alias for includes



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

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

#includes(*fields) ⇒ self

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

Examples:

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

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

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

Parameters:

  • fields (Array)

    the list of Pointer columns to fetch.

Returns:

  • (self)


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

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

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

Note:

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

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

Examples:

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

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

Parameters:

  • fields (Array)

    the name of the fields to return.

Returns:

  • (self)


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

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

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

Note:

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

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

Examples:

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

Parameters:

  • limit (Integer) (defaults to: 1)

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

Returns:



1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
# File 'lib/parse/query.rb', line 1441

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:



1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
# File 'lib/parse/query.rb', line 1419

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

#limit(count) ⇒ self

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

Examples:

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

Parameters:

  • count (Integer, Symbol, String, nil)

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

Returns:

  • (self)


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

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

  @results = nil
  self #chaining
end

#map { ... } ⇒ Array

Yields:

  • a block yield for each object in the result

Returns:

See Also:

  • Array#map


1342
1343
1344
1345
# File 'lib/parse/query.rb', line 1342

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.



4288
4289
4290
4291
4292
4293
4294
4295
4296
4297
4298
4299
4300
4301
4302
# File 'lib/parse/query.rb', line 4288

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.



4269
4270
4271
4272
4273
4274
4275
4276
4277
4278
4279
4280
4281
4282
4283
# File 'lib/parse/query.rb', line 4269

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:



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

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:



5622
5623
5624
5625
5626
# File 'lib/parse/query.rb', line 5622

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

#or_where(where_clauses = []) ⇒ Query

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

Examples:

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

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

Parameters:

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

    a list of Parse::Constraint objects to combine.

Returns:

  • (Query)

    the combined query with an OR clause.



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

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

#order(*ordering) ⇒ self

Add a sorting order for the query.

Examples:

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

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

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

Parameters:

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

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

Returns:

  • (self)


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

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

#pipelineArray

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

Returns:

  • (Array)

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



4198
4199
4200
4201
4202
4203
4204
4205
4206
4207
4208
4209
4210
4211
4212
# File 'lib/parse/query.rb', line 4198

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



3723
3724
3725
3726
3727
# File 'lib/parse/query.rb', line 3723

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

#pluck(field) ⇒ Array

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

Examples:

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

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

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

Parameters:

  • field (Symbol, String)

    the field name to extract values for.

Returns:

  • (Array)

    an array of field values from all matching objects.



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

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

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

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

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

#prepared(includeClassName: false) ⇒ Hash

Returns a compiled query without encoding the where clause.

Parameters:

  • includeClassName (Boolean) (defaults to: false)

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

Returns:

  • (Hash)

    a hash representing the prepared query request.



4123
4124
4125
# File 'lib/parse/query.rb', line 4123

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

#prettyString

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

Returns:



4222
4223
4224
# File 'lib/parse/query.rb', line 4222

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:



5595
5596
5597
5598
# File 'lib/parse/query.rb', line 5595

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:



5567
5568
5569
# File 'lib/parse/query.rb', line 5567

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:



5581
5582
5583
# File 'lib/parse/query.rb', line 5581

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:



5543
5544
5545
# File 'lib/parse/query.rb', line 5543

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:



5555
5556
5557
# File 'lib/parse/query.rb', line 5555

def publicly_writable(mongo_direct: nil)
  writable_by("*", mongo_direct: mongo_direct)
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



2028
2029
2030
# File 'lib/parse/query.rb', line 2028

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

#read_pref(preference) ⇒ self

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

Examples:

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

Parameters:

  • preference (Symbol, String)

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

Returns:

  • (self)


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

def read_pref(preference)
  @read_preference = preference
  self
end

#readable_by(permission, mongo_direct: nil) ⇒ 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")           # Objects readable by user ID
Song.query.readable_by("role:Admin")        # Objects readable by Admin role
Song.query.readable_by(current_user)        # Objects readable by user object
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([], mongo_direct: true)  # Force MongoDB direct query

Parameters:

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

    the permission to check

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

Returns:



5471
5472
5473
5474
5475
# File 'lib/parse/query.rb', line 5471

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

#readable_by_role(role_name, mongo_direct: nil) ⇒ 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.

Returns:



5486
5487
5488
5489
5490
# File 'lib/parse/query.rb', line 5486

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

Raises:

  • (ArgumentError)


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

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

#requires_aggregation?Boolean

Check if this query requires aggregation pipeline execution

Returns:

  • (Boolean)

    true if the query contains pipeline-based constraints



4216
4217
4218
# File 'lib/parse/query.rb', line 4216

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



2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
# File 'lib/parse/query.rb', line 2009

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)


1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
# File 'lib/parse/query.rb', line 1782

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:



2036
2037
2038
# File 'lib/parse/query.rb', line 2036

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.



1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
# File 'lib/parse/query.rb', line 1706

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:



2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
# File 'lib/parse/query.rb', line 2065

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)



3135
3136
3137
3138
3139
3140
3141
3142
3143
3144
3145
3146
3147
3148
3149
3150
3151
3152
3153
3154
3155
3156
3157
3158
3159
# File 'lib/parse/query.rb', line 3135

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)


1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
# File 'lib/parse/query.rb', line 1878

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)


1843
1844
1845
1846
1847
1848
# File 'lib/parse/query.rb', line 1843

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


1350
1351
1352
1353
# File 'lib/parse/query.rb', line 1350

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

#skip(amount) ⇒ self

Use with limit to paginate through results. Default is 0.

Examples:

# get the next 3 songs after the first 10
Song.all :limit => 3, :skip => 10

Parameters:

  • amount (Integer)

    The number of records to skip.

Returns:

  • (self)


778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
# File 'lib/parse/query.rb', line 778

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

#subscribe(fields: nil, keys: nil, watch: nil, session_token: nil, client: nil, use_master_key: false) {|subscription| ... } ⇒ Parse::LiveQuery::Subscription

Subscribe to real-time updates for objects matching this query. Uses Parse LiveQuery WebSocket connection to receive push notifications when objects are created, updated, deleted, or enter/leave the query results.

Examples:

Basic subscription

subscription = Song.query(:artist => "Beatles").subscribe
subscription.on(:create) { |song| puts "New song: #{song.title}" }
subscription.on(:update) { |song, original| puts "Updated!" }
subscription.on(:delete) { |song| puts "Deleted: #{song.id}" }

With field filtering

subscription = User.query(:status => "active").subscribe(fields: ["name", "email"])
subscription.on_update { |user| puts "User updated: #{user.name}" }

With session token for ACL-aware subscriptions

subscription = PrivateData.query.subscribe(session_token: current_user.session_token)

Parameters:

  • fields (Array<String>) (defaults to: nil)

    specific fields to watch for changes (nil = all fields)

  • session_token (String) (defaults to: nil)

    session token for ACL-aware subscriptions

  • client (Parse::LiveQuery::Client) (defaults to: nil)

    custom LiveQuery client (optional)

  • use_master_key (Boolean) (defaults to: false)

    an intent assertion, NOT a per-subscription elevation. Parse Server resolves masterKey once, at connect time, from the LiveQuery connect frame; the subscribe frame never carries it. This flag therefore only has effect when the underlying client is itself an admin connection (Parse::LiveQuery::Client.new(use_master_key: true) with a master key), in which case the entire socket is already elevated and ALL its subscriptions bypass ACL/CLP. On a non-admin connection use_master_key: true does not elevate the subscription and emits a security warning. A single socket cannot mix scoped and admin subscriptions — use separate connections for end-user (session-token-scoped) versus administrative (master-key-scoped) work.

Yields:

  • (subscription)

    runs the block with the freshly-constructed LiveQuery::Subscription BEFORE the subscribe frame is sent so caller-registered callbacks are wired before any server events can arrive. Optional.

Returns:

See Also:



3230
3231
3232
3233
3234
3235
3236
3237
3238
3239
3240
3241
3242
3243
3244
# File 'lib/parse/query.rb', line 3230

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.



4229
4230
4231
4232
4233
4234
4235
4236
4237
4238
4239
4240
4241
4242
4243
# File 'lib/parse/query.rb', line 4229

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


1357
1358
1359
# File 'lib/parse/query.rb', line 1357

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:



4077
4078
4079
4080
4081
4082
4083
4084
4085
4086
4087
4088
4089
4090
4091
4092
4093
4094
4095
4096
4097
4098
4099
4100
4101
4102
4103
4104
4105
4106
4107
4108
4109
4110
4111
4112
# File 'lib/parse/query.rb', line 4077

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



4444
4445
4446
4447
4448
4449
4450
4451
4452
4453
4454
4455
4456
4457
4458
4459
4460
4461
4462
4463
4464
4465
4466
4467
4468
4469
4470
4471
4472
# File 'lib/parse/query.rb', line 4444

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



3496
3497
3498
3499
# File 'lib/parse/query.rb', line 3496

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.



3527
3528
3529
3530
3531
# File 'lib/parse/query.rb', line 3527

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.



3517
3518
3519
3520
3521
# File 'lib/parse/query.rb', line 3517

def validate_pipeline!(pipeline)
  Parse::PipelineSecurity.validate_filter!(pipeline)
rescue Parse::PipelineSecurity::Error => e
  raise ArgumentError, e.message
end

#where(expressions = nil, opts = {}) ⇒ self

Add additional query constraints to the where clause. The where clause is based on utilizing a set of constraints on the defined column names in your Parse classes. The constraints are implemented as method operators on field names that are tied to a value. Any symbol/string that is not one of the main expression keywords described here will be considered as a type of query constraint for the where clause in the query.

Examples:

# parts of a single where constraint
{ :column.constraint => value }

Parameters:

  • conditions (Hash)

    a set of constraints for this query.

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

    a set of options when adding the constraints. This is specific for each Parse::Constraint.

Returns:

  • (self)

See Also:



1036
1037
1038
1039
1040
1041
1042
1043
# File 'lib/parse/query.rb', line 1036

def where(expressions = nil, opts = {})
  return @where if expressions.nil?
  if expressions.is_a?(Hash)
    # Route through conditions to handle special keywords like :keys, :include, etc.
    conditions(expressions)
  end
  self #chaining
end

#where_constraintsHash

Formats the current set of Parse::Constraint instances in the where clause as an expression hash.

Returns:

  • (Hash)

    the set of constraints



1018
1019
1020
# File 'lib/parse/query.rb', line 1018

def where_constraints
  @where.reduce({}) { |memo, constraint| memo[constraint.operation] = constraint.value; memo }
end

#writable_by(permission, mongo_direct: nil) ⇒ 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")           # Objects writable by user ID
Song.query.writable_by("role:Admin")        # Objects writable by Admin role
Song.query.writable_by(current_user)        # Objects writable by user object
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([], mongo_direct: true)  # Force MongoDB direct query

Parameters:

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

    the permission to check

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

Returns:



5510
5511
5512
5513
5514
# File 'lib/parse/query.rb', line 5510

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

#writable_by_role(role_name, mongo_direct: nil) ⇒ 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.

Returns:



5525
5526
5527
5528
5529
# File 'lib/parse/query.rb', line 5525

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

#|(other_query) ⇒ Query

Returns the combined query with an OR clause.

Returns:

  • (Query)

    the combined query with an OR clause.

Raises:

  • (ArgumentError)

See Also:



1091
1092
1093
1094
1095
1096
# File 'lib/parse/query.rb', line 1091

def |(other_query)
  raise ArgumentError, "Parse queries must be of the same class #{@table}." unless @table == other_query.table
  copy_query = self.clone
  copy_query.or_where other_query.where
  copy_query
end