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


475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
# File 'lib/parse/query.rb', line 475

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 = []
  @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
  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:



1711
1712
1713
# File 'lib/parse/query.rb', line 1711

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:



1707
1708
1709
# File 'lib/parse/query.rb', line 1707

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



5167
5168
5169
5170
5171
5172
5173
5174
5175
5176
5177
5178
5179
5180
5181
5182
5183
5184
5185
5186
5187
5188
5189
5190
5191
5192
5193
5194
5195
5196
5197
5198
# File 'lib/parse/query.rb', line 5167

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.



384
385
386
# File 'lib/parse/query.rb', line 384

def compile_markers(where)
  constraint_reduce(where)
end

.compile_where(where) ⇒ Hash

This methods takes a set of constraints and merges them to build a final where constraint clause for sending to the Parse backend.

__-prefixed internal routing markers (e.g. "__mongo_direct_only" and "__aggregation_pipeline") are stripped from the returned hash — they are SDK-internal hints that must never reach Parse REST or MongoDB. Use compile_markers (instance method #compile_markers) to retrieve them for routing decisions / pipeline assembly.

Parameters:

Returns:

  • (Hash)

    a hash representing the compiled query, with internal routing markers stripped.



373
374
375
# File 'lib/parse/query.rb', line 373

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



5132
5133
5134
5135
5136
5137
5138
5139
5140
5141
5142
5143
5144
5145
5146
5147
5148
5149
5150
5151
5152
5153
5154
5155
5156
5157
5158
5159
5160
# File 'lib/parse/query.rb', line 5132

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:



856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
# File 'lib/parse/query.rb', line 856

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)


834
835
836
837
838
# File 'lib/parse/query.rb', line 834

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) ⇒ Object Also known as: aggregate_pipeline



3134
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
3160
3161
3162
3163
3164
3165
3166
3167
3168
3169
3170
3171
3172
3173
3174
3175
3176
3177
3178
3179
3180
3181
3182
3183
3184
3185
3186
3187
3188
3189
3190
3191
3192
3193
3194
3195
3196
3197
3198
3199
3200
3201
3202
3203
3204
3205
3206
3207
3208
3209
3210
3211
3212
3213
3214
3215
3216
3217
3218
3219
3220
3221
3222
3223
3224
3225
3226
3227
3228
3229
3230
3231
3232
3233
3234
3235
3236
3237
3238
3239
3240
3241
3242
3243
3244
3245
3246
3247
3248
3249
3250
3251
3252
3253
3254
3255
3256
3257
3258
3259
3260
3261
3262
3263
3264
3265
3266
3267
3268
3269
3270
3271
3272
3273
3274
3275
3276
3277
3278
3279
# File 'lib/parse/query.rb', line 3134

def aggregate(pipeline, verbose: nil, mongo_direct: nil, rewrite_lookups: nil)
  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)
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



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

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:



3780
3781
3782
3783
3784
# File 'lib/parse/query.rb', line 3780

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

#as_json(*args) ⇒ Hash

Returns:



3906
3907
3908
# File 'lib/parse/query.rb', line 3906

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:



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

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:



2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
# File 'lib/parse/query.rb', line 2364

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:



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
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
# File 'lib/parse/query.rb', line 2199

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)

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



4027
4028
4029
4030
4031
4032
4033
4034
4035
4036
4037
4038
4039
4040
4041
# File 'lib/parse/query.rb', line 4027

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]



3523
3524
3525
3526
3527
3528
3529
3530
3531
3532
3533
3534
3535
3536
3537
3538
3539
3540
3541
3542
3543
3544
3545
3546
3547
3548
3549
3550
3551
3552
3553
3554
3555
3556
3557
3558
3559
3560
3561
3562
3563
3564
3565
3566
3567
3568
3569
3570
3571
3572
3573
3574
3575
3576
3577
3578
3579
3580
3581
3582
3583
3584
3585
3586
3587
3588
3589
3590
3591
3592
3593
3594
3595
3596
3597
3598
3599
3600
3601
3602
3603
3604
3605
3606
3607
3608
3609
3610
3611
3612
3613
3614
3615
3616
3617
3618
3619
3620
3621
3622
3623
3624
3625
3626
3627
3628
3629
3630
3631
# File 'lib/parse/query.rb', line 3523

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



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
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
# File 'lib/parse/query.rb', line 2399

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



3732
3733
3734
3735
3736
3737
3738
3739
3740
3741
3742
3743
3744
3745
3746
3747
3748
3749
3750
# File 'lib/parse/query.rb', line 3732

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



2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
# File 'lib/parse/query.rb', line 2497

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.



586
587
588
589
# File 'lib/parse/query.rb', line 586

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)


434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
# File 'lib/parse/query.rb', line 434

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



5206
5207
5208
5209
5210
5211
5212
5213
5214
5215
5216
5217
5218
5219
5220
5221
5222
5223
5224
5225
5226
5227
5228
5229
5230
# File 'lib/parse/query.rb', line 5206

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, :includes, :limit, :skip, :cache, :use_master_key].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:



3924
3925
3926
3927
3928
3929
3930
3931
3932
3933
3934
3935
3936
3937
3938
3939
3940
3941
3942
3943
3944
3945
3946
3947
3948
3949
3950
3951
3952
3953
3954
3955
3956
3957
# File 'lib/parse/query.rb', line 3924

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[: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
    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.



3961
3962
3963
# File 'lib/parse/query.rb', line 3961

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)


507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
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
# File 'lib/parse/query.rb', line 507

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.



927
928
929
# File 'lib/parse/query.rb', line 927

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.



2899
2900
2901
2902
2903
2904
2905
2906
2907
# File 'lib/parse/query.rb', line 2899

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



2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
# File 'lib/parse/query.rb', line 2591

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



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

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.



2883
2884
2885
2886
2887
2888
2889
2890
2891
# File 'lib/parse/query.rb', line 2883

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.



2827
2828
2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
2839
2840
# File 'lib/parse/query.rb', line 2827

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.



2849
2850
2851
2852
2853
2854
2855
2856
2857
# File 'lib/parse/query.rb', line 2849

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



2914
2915
2916
2917
2918
2919
2920
2921
2922
2923
# File 'lib/parse/query.rb', line 2914

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.



2861
2862
2863
2864
2865
2866
2867
2868
2869
2870
# File 'lib/parse/query.rb', line 2861

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



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

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



2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
# File 'lib/parse/query.rb', line 2734

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



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

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:



2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
# File 'lib/parse/query.rb', line 2007

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)

  # 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



1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
# File 'lib/parse/query.rb', line 1212

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:



2992
2993
2994
# File 'lib/parse/query.rb', line 2992

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:



3789
3790
3791
3792
3793
3794
3795
3796
3797
3798
# File 'lib/parse/query.rb', line 3789

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



3083
3084
3085
3086
3087
3088
3089
3090
3091
3092
3093
3094
3095
3096
3097
3098
3099
3100
3101
3102
3103
3104
3105
3106
3107
3108
# File 'lib/parse/query.rb', line 3083

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



1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
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
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
# File 'lib/parse/query.rb', line 1032

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:



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
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
# File 'lib/parse/query.rb', line 2070

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



2153
2154
2155
2156
2157
2158
# File 'lib/parse/query.rb', line 2153

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.



4298
4299
4300
4301
4302
4303
4304
4305
# File 'lib/parse/query.rb', line 4298

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



1125
1126
1127
# File 'lib/parse/query.rb', line 1125

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)


1614
1615
1616
1617
1618
1619
# File 'lib/parse/query.rb', line 1614

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


1250
1251
1252
1253
# File 'lib/parse/query.rb', line 1250

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

#execute_aggregation_pipelineAggregation

Execute an aggregation pipeline for queries with pipeline constraints

Returns:

  • (Aggregation)

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



3485
3486
3487
3488
3489
3490
3491
3492
3493
3494
3495
3496
3497
3498
3499
3500
3501
3502
3503
3504
3505
3506
3507
3508
3509
# File 'lib/parse/query.rb', line 3485

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



3065
3066
3067
3068
3069
3070
3071
3072
3073
3074
# File 'lib/parse/query.rb', line 3065

def explain
  compiled_query = compile
  compiled_query[:explain] = true
  response = client.find_objects(@table, compiled_query.as_json, **_opts)
  if response.error?
    puts "[ParseQuery:Explain] #{response.error}"
    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



3639
3640
3641
3642
3643
3644
3645
3646
3647
3648
3649
3650
3651
3652
3653
3654
3655
3656
3657
3658
3659
3660
3661
3662
3663
3664
3665
3666
3667
3668
3669
3670
3671
3672
3673
3674
3675
3676
3677
3678
3679
3680
3681
3682
3683
3684
3685
3686
3687
3688
3689
3690
3691
3692
3693
3694
3695
3696
3697
3698
3699
3700
3701
3702
3703
3704
3705
3706
3707
3708
3709
3710
3711
3712
3713
3714
3715
3716
3717
3718
3719
3720
3721
3722
3723
3724
3725
3726
3727
# File 'lib/parse/query.rb', line 3639

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:



1488
1489
1490
1491
1492
1493
1494
# File 'lib/parse/query.rb', line 1488

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.



1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
# File 'lib/parse/query.rb', line 1286

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:



1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
# File 'lib/parse/query.rb', line 1957

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:



1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
# File 'lib/parse/query.rb', line 1373

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



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

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:



4110
4111
4112
4113
4114
4115
4116
4117
4118
4119
4120
# File 'lib/parse/query.rb', line 4110

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:



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

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.



4144
4145
4146
4147
4148
4149
4150
4151
4152
4153
4154
4155
4156
4157
4158
4159
4160
4161
4162
4163
4164
4165
4166
4167
4168
4169
4170
4171
4172
4173
4174
4175
4176
4177
4178
4179
4180
4181
4182
4183
4184
4185
4186
# File 'lib/parse/query.rb', line 4144

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



3755
3756
3757
3758
3759
3760
3761
3762
3763
3764
3765
3766
3767
3768
3769
# File 'lib/parse/query.rb', line 3755

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



827
828
829
# File 'lib/parse/query.rb', line 827

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)


814
815
816
817
818
819
820
821
822
823
824
# File 'lib/parse/query.rb', line 814

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)


605
606
607
608
609
610
611
612
613
614
615
# File 'lib/parse/query.rb', line 605

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:



1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
# File 'lib/parse/query.rb', line 1357

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:



1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
# File 'lib/parse/query.rb', line 1335

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)


757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
# File 'lib/parse/query.rb', line 757

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


1258
1259
1260
1261
# File 'lib/parse/query.rb', line 1258

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.



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

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.



4048
4049
4050
4051
4052
4053
4054
4055
4056
4057
4058
4059
4060
4061
4062
# File 'lib/parse/query.rb', line 4048

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:



5388
5389
5390
5391
5392
# File 'lib/parse/query.rb', line 5388

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:



5401
5402
5403
5404
5405
# File 'lib/parse/query.rb', line 5401

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.



977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
# File 'lib/parse/query.rb', line 977

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)


678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
# File 'lib/parse/query.rb', line 678

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



3977
3978
3979
3980
3981
3982
3983
3984
3985
3986
3987
3988
3989
3990
3991
# File 'lib/parse/query.rb', line 3977

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



3514
3515
3516
3517
3518
# File 'lib/parse/query.rb', line 3514

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.



636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
# File 'lib/parse/query.rb', line 636

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.



3914
3915
3916
# File 'lib/parse/query.rb', line 3914

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

#prettyString

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

Returns:



4001
4002
4003
# File 'lib/parse/query.rb', line 4001

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:



5374
5375
5376
5377
# File 'lib/parse/query.rb', line 5374

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:



5346
5347
5348
# File 'lib/parse/query.rb', line 5346

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:



5360
5361
5362
# File 'lib/parse/query.rb', line 5360

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:



5322
5323
5324
# File 'lib/parse/query.rb', line 5322

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:



5334
5335
5336
# File 'lib/parse/query.rb', line 5334

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



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

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)


790
791
792
793
# File 'lib/parse/query.rb', line 790

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:



5250
5251
5252
5253
5254
# File 'lib/parse/query.rb', line 5250

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:



5265
5266
5267
5268
5269
# File 'lib/parse/query.rb', line 5265

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)


795
796
797
798
799
# File 'lib/parse/query.rb', line 795

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



3995
3996
3997
# File 'lib/parse/query.rb', line 3995

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



1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
# File 'lib/parse/query.rb', line 1824

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)


1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
# File 'lib/parse/query.rb', line 1597

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:



1851
1852
1853
# File 'lib/parse/query.rb', line 1851

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.



1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
# File 'lib/parse/query.rb', line 1521

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:



1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
# File 'lib/parse/query.rb', line 1880

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)

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

  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)



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

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)


1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
# File 'lib/parse/query.rb', line 1693

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)


1658
1659
1660
1661
1662
1663
# File 'lib/parse/query.rb', line 1658

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


1266
1267
1268
1269
# File 'lib/parse/query.rb', line 1266

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)


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

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



3035
3036
3037
3038
3039
3040
3041
3042
3043
3044
3045
3046
3047
# File 'lib/parse/query.rb', line 3035

def subscribe(fields: 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,
    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.



4008
4009
4010
4011
4012
4013
4014
4015
4016
4017
4018
4019
4020
4021
4022
# File 'lib/parse/query.rb', line 4008

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


1273
1274
1275
# File 'lib/parse/query.rb', line 1273

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:



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

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



4223
4224
4225
4226
4227
4228
4229
4230
4231
4232
4233
4234
4235
4236
4237
4238
4239
4240
4241
4242
4243
4244
4245
4246
4247
4248
4249
4250
4251
# File 'lib/parse/query.rb', line 4223

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



3287
3288
3289
3290
# File 'lib/parse/query.rb', line 3287

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.



3318
3319
3320
3321
3322
# File 'lib/parse/query.rb', line 3318

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.



3308
3309
3310
3311
3312
# File 'lib/parse/query.rb', line 3308

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:



952
953
954
955
956
957
958
959
# File 'lib/parse/query.rb', line 952

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



934
935
936
# File 'lib/parse/query.rb', line 934

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:



5289
5290
5291
5292
5293
# File 'lib/parse/query.rb', line 5289

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:



5304
5305
5306
5307
5308
# File 'lib/parse/query.rb', line 5304

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:



1007
1008
1009
1010
1011
1012
# File 'lib/parse/query.rb', line 1007

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