Class: Parse::Query
- Inherits:
-
Object
- Object
- Parse::Query
- 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
- 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!andParse::Object.create_or_update!, which must hand a Hash containing ONLY constraint key/value pairs toParse::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.
Parse::PipelineSecurity::DENIED_OPERATORS
Class Attribute Summary collapse
-
.allow_scope_introspection ⇒ Symbol
The method to use when converting field names to Parse column names.
-
.field_formatter ⇒ Symbol
The method to use when converting field names to Parse column names.
Instance Attribute Summary collapse
-
#acl_role ⇒ Parse::Role, ...
readonly
The role the query was scoped to via #scope_to_role, or nil.
-
#acl_user ⇒ Parse::User, ...
readonly
The user the query was scoped to via #scope_to_user, or nil for unscoped queries.
-
#cache ⇒ Boolean, Integer
Set whether this query should be cached and for how long.
-
#client ⇒ Parse::Client
The client to use for making the API request.
-
#key ⇒ String
This parameter is used to support
selectqueries where you have to pass akeyparameter for matching different tables. -
#read_preference ⇒ Symbol, String
Set the MongoDB read preference for this query.
-
#session_token ⇒ Object
Returns the value of attribute session_token.
-
#table ⇒ String
The name of the Parse collection to query against.
-
#use_master_key ⇒ Boolean
True or false on whether we should send the master key in this request.
-
#verbose_aggregate ⇒ Object
Returns the value of attribute verbose_aggregate.
Class Method Summary collapse
-
.all(table, constraints = { limit: :max }) ⇒ Query
Helper method to create a query with constraints for a specific Parse collection.
-
.and(*queries) ⇒ Parse::Query
Combines multiple queries with AND logic using full pipeline approach Each query's complete constraint set is ANDed together.
-
.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"). -
.compile_where(where) ⇒ Hash
This methods takes a set of constraints and merges them to build a final
whereconstraint clause for sending to the Parse backend. -
.format_field(str) ⇒ String
Formatted string using Query.field_formatter.
-
.known_parse_classes ⇒ Object
Known Parse classes for fast validation - dynamically loaded from schema.
-
.option_key?(key) ⇒ Boolean
Whether
keyis one of the QUERY_OPTION_KEYS that #conditions absorbs as a query-shape option rather than a field-name constraint. -
.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.
-
.parse_keys_to_nested_keys(keys) ⇒ Hash
Parses keys patterns to build a map of nested fetched keys.
-
.pointer_shape_warned ⇒ Object
Process-wide
[table, field]cache for warn-once dedup in #handle_unresolvable_pointer_in_array!. -
.reset_known_parse_classes! ⇒ Object
Allow resetting the cached known classes (useful for testing).
-
.to_snake_case(str) ⇒ String
Convert camelCase string to snake_case.
Instance Method Summary collapse
-
#add_constraint(operator, value = nil, opts = {}) ⇒ self
Add a constraint to the query.
-
#add_constraints(list) ⇒ self
Combine a list of Constraint objects.
-
#after_prepare { ... } ⇒ Object
A callback called after the query is compiled.
- #aggregate(pipeline, verbose: nil, mongo_direct: nil, rewrite_lookups: nil) ⇒ Object (also: #aggregate_pipeline)
-
#aggregate_from_query(additional_stages = [], verbose: nil, mongo_direct: nil) ⇒ Aggregation
Converts the current query into an aggregate pipeline and executes it.
-
#all(expressions = { limit: :max }) { ... } ⇒ Array<Hash>, Array<Parse::Object>
Similar to #results but takes an additional set of conditions to apply.
- #as_json(*args) ⇒ Hash
-
#atlas_autocomplete(query, field:, **options) ⇒ Parse::AtlasSearch::AutocompleteResult
Execute an autocomplete search using MongoDB Atlas Search.
-
#atlas_facets(query, facets, **options) ⇒ Parse::AtlasSearch::FacetedResult
Execute a faceted search using MongoDB Atlas Search.
-
#atlas_search(query = nil, **options) {|SearchBuilder| ... } ⇒ Parse::AtlasSearch::SearchResult
Execute a full-text search using MongoDB Atlas Search.
-
#average(field) ⇒ Float
(also: #avg)
Calculate the average of values for a specific field.
-
#before_prepare { ... } ⇒ Object
A callback called before the query is compiled.
-
#build_aggregation_pipeline ⇒ Array
Build the complete aggregation pipeline from constraints Pipeline order: $match (regular) -> $lookup (subqueries) -> $match (post-lookup) -> $match (aggregation) -> non-$match stages -> limit/skip.
-
#build_direct_mongodb_pipeline ⇒ Array<Hash>
private
Build an aggregation pipeline optimized for direct MongoDB execution.
-
#build_filter_condition(where) ⇒ Hash
Build a $filter condition expression from where constraints.
-
#build_include_lookup_stages(includes) ⇒ Array<Hash>
private
Build $lookup stages for included pointer fields in direct MongoDB queries.
-
#clause(clause_name = :where) ⇒ Object
returns the query clause for the particular clause.
-
#clear(item = :results) ⇒ self
Clear a specific clause of this query.
-
#clone ⇒ Parse::Query
Creates a deep copy of this query object, allowing independent modifications.
-
#compile(encode: true, includeClassName: false) ⇒ Hash
Complies the query and runs all prepare callbacks.
-
#compile_where ⇒ Hash
A hash representing just the
whereclause of this query, with SDK-internal routing markers stripped. -
#conditions(expressions = {}) ⇒ self
(also: #query, #append)
Add a set of query expressions and constraints.
- #constraints(raw = false) ⇒ Array<Parse::Constraint>, Hash
-
#convert_addfields_for_direct_mongodb(spec) ⇒ Object
private
Convert a $addFields / $set stage for direct MongoDB.
-
#convert_constraints_for_direct_mongodb(constraints) ⇒ Hash
private
Convert constraints for direct MongoDB execution.
-
#convert_field_for_direct_mongodb(field) ⇒ String
private
Convert a field name for direct MongoDB access.
-
#convert_group_for_direct_mongodb(group) ⇒ Object
private
Convert $group stage for direct MongoDB.
-
#convert_match_for_direct_mongodb(match) ⇒ Object
private
Convert a $match stage for direct MongoDB.
-
#convert_projection_for_direct_mongodb(projection) ⇒ Object
private
Convert projection fields for direct MongoDB.
-
#convert_replace_root_for_direct_mongodb(spec) ⇒ Object
private
Convert a $replaceRoot stage for direct MongoDB.
-
#convert_sort_for_direct_mongodb(sort) ⇒ Object
private
Convert sort specification for direct MongoDB.
-
#convert_stage_for_direct_mongodb(stage) ⇒ Hash
private
Convert an aggregation stage for direct MongoDB execution.
-
#convert_value_for_direct_mongodb(field, value) ⇒ Object
private
Convert a value for direct MongoDB execution.
-
#count(mongo_direct: false) ⇒ Integer
Perform a count query.
-
#count_direct(session_token: nil, master: nil, acl_user: nil, acl_role: nil) ⇒ Integer
Execute a count query directly against MongoDB, bypassing Parse Server.
-
#count_distinct(field) ⇒ Integer
Perform a count distinct query using MongoDB aggregation pipeline.
-
#cursor(limit: 100, order: nil) ⇒ Parse::Cursor
Create a cursor-based paginator for efficiently traversing large datasets.
-
#decode(list) ⇒ Array<Parse::Object>
Builds objects based on the set of Parse JSON hashes in an array.
-
#deduplicate_consecutive_match_stages(pipeline) ⇒ Array<Hash>
private
Merge consecutive $match stages in an aggregation pipeline.
-
#distinct(field, return_pointers: false, mongo_direct: false, order: nil) ⇒ Object
Queries can be made using distinct, allowing you find unique values for a specified field.
-
#distinct_direct(field, return_pointers: false, order: nil, session_token: nil, master: nil, acl_user: nil, acl_role: nil) ⇒ Array
Execute a distinct query directly against MongoDB, bypassing Parse Server.
-
#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.
-
#distinct_objects(field, return_pointers: false) ⇒ Array
Enhanced distinct method that automatically populates Parse pointer objects at the server level.
-
#distinct_pointers(field, order: nil) ⇒ Array
Convenience method for distinct queries that always return Parse::Pointer objects for pointer fields.
-
#distinct_query_is_scoped? ⇒ Boolean
private
Whether this query carries a non-master-key auth scope.
- #each { ... } ⇒ Array
-
#execute_aggregation_pipeline ⇒ Aggregation
Execute an aggregation pipeline for queries with pipeline constraints.
-
#explain ⇒ Hash
Returns the query execution plan from MongoDB.
-
#extract_subquery_to_lookup_stages(constraints) ⇒ Hash
Extract $inQuery and $notInQuery constraints and build $lookup stages for them.
-
#fetch!(compiled_query) ⇒ Parse::Response
(also: #execute!)
Performs the fetch request for the query.
- #first(limit_or_constraints = 1, mongo_direct: false, **options) ⇒ Object
-
#first_direct(limit_or_constraints = 1) ⇒ Parse::Object, ...
Execute the query directly against MongoDB and return the first result.
-
#get(object_id) ⇒ Parse::Object
Retrieve a single object by its objectId.
-
#get_pointer_target_class(field) ⇒ String?
private
Get the target class name for a pointer field from model references.
-
#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.
-
#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.
-
#group_objects_by(field, return_pointers: false) ⇒ Hash
Group Parse objects by a field value and return arrays of actual objects.
-
#has_subquery_constraints?(constraints) ⇒ Boolean
Check if constraints contain $inQuery or $notInQuery that need resolution.
-
#include(*fields) ⇒ Object
alias for includes.
-
#includes(*fields) ⇒ self
Set a list of Parse Pointer columns to be fetched for matching records.
-
#initialize(table, constraints = {}) ⇒ Query
constructor
Constructor method to create a query with constraints for a specific Parse collection.
-
#keys(*fields) ⇒ self
(also: #select_fields)
Restrict the fields returned by the query.
-
#last_updated(limit = 1, **options) ⇒ Parse::Object+
Returns the most recently updated object(s) (ordered by updated_at descending).
-
#latest(limit = 1, **options) ⇒ Parse::Object+
Returns the most recently created object(s) (ordered by created_at descending).
-
#limit(count) ⇒ self
Limit the number of objects returned by the query.
- #map { ... } ⇒ Array
-
#max(field) ⇒ Object
Find the maximum value for a specific field.
-
#min(field) ⇒ Object
Find the minimum value for a specific field.
-
#not_publicly_readable(mongo_direct: nil) ⇒ Parse::Query
Find objects that are NOT publicly readable.
-
#not_publicly_writable(mongo_direct: nil) ⇒ Parse::Query
Find objects that are NOT publicly writable.
-
#or_where(where_clauses = []) ⇒ Query
Combine two where clauses into an OR constraint.
-
#order(*ordering) ⇒ self
Add a sorting order for the query.
-
#pipeline ⇒ Array
Returns the aggregation pipeline for this query if it contains pipeline-based constraints.
-
#pipeline_uses_internal_fields?(pipeline) ⇒ Boolean
Check if the pipeline references internal Parse fields that require MongoDB direct access.
-
#pluck(field) ⇒ Array
Extract values for a specific field from all matching objects.
-
#prepared(includeClassName: false) ⇒ Hash
Returns a compiled query without encoding the where clause.
-
#pretty ⇒ String
Retruns a formatted JSON string representing the query, useful for debugging.
-
#private_acl(mongo_direct: nil) ⇒ Parse::Query
(also: #master_key_only)
Find objects with completely private ACL (no read AND no write permissions).
-
#privately_readable(mongo_direct: nil) ⇒ Parse::Query
(also: #master_key_read_only)
Find objects with no read permissions (master key only).
-
#privately_writable(mongo_direct: nil) ⇒ Parse::Query
(also: #master_key_write_only)
Find objects with no write permissions (master key only).
-
#publicly_readable(mongo_direct: nil) ⇒ Parse::Query
Find objects that are publicly readable (anyone can read).
-
#publicly_writable(mongo_direct: nil) ⇒ Parse::Query
Find objects that are publicly writable (anyone can write).
-
#raw { ... } ⇒ Array<Hash>
Returns raw unprocessed results from the query (hash format).
-
#read_pref(preference) ⇒ self
Set the MongoDB read preference for this query.
-
#readable_by(permission, mongo_direct: nil) ⇒ Parse::Query
Filter by ACL read permissions using exact permission strings.
-
#readable_by_role(role_name, mongo_direct: nil) ⇒ Parse::Query
Filter by ACL read permissions using role names (adds "role:" prefix).
- #related_to(field, pointer) ⇒ Object
-
#requires_aggregation? ⇒ Boolean
Check if this query requires aggregation pipeline execution.
-
#requires_aggregation_pipeline? ⇒ Boolean
Check if this query contains constraints that require aggregation pipeline processing.
-
#requires_mongo_direct? ⇒ Boolean
Check if this query contains a constraint that can only be answered via mongo-direct (e.g.
$geoIntersectswith a full$geometryagainst a non-GeoPoint column — an operator Parse Server's REST find layer does not expose). -
#result_pointers { ... } ⇒ Array<Parse::Pointer>
(also: #results_pointers)
Returns only pointer objects for all matching results This is memory efficient for large result sets where you only need pointers.
-
#results(raw: false, return_pointers: false, mongo_direct: false) { ... } ⇒ Array<Hash>, Array<Parse::Object>
(also: #result)
Executes the query and builds the result set of Parse::Objects that matched.
-
#results_direct(raw: false, max_time_ms: nil, session_token: nil, master: nil, acl_user: nil, acl_role: nil) { ... } ⇒ Array<Parse::Object>, Array<Hash>
Execute the query directly against MongoDB, bypassing Parse Server.
-
#rewrite_expression_for_direct_mongodb(expr) ⇒ Object
private
Recursively rewrite field references inside an aggregation expression to their direct-MongoDB column names.
-
#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.
-
#scope_to_user(user) ⇒ self
Scope a query to a specific user's row-level ACL when it auto-routes through mongo-direct.
- #select { ... } ⇒ Array
-
#skip(amount) ⇒ self
Use with limit to paginate through results.
-
#subscribe(fields: nil, session_token: nil, client: nil) ⇒ Parse::LiveQuery::Subscription
Subscribe to real-time updates for objects matching this query.
-
#sum(field) ⇒ Numeric
Calculate the sum of values for a specific field.
- #to_a ⇒ Array
-
#to_pointers(list, field = nil) ⇒ Array<Parse::Pointer>
Builds Parse::Pointer objects based on the set of Parse JSON hashes in an array.
-
#to_table(columns = nil, format: :ascii, headers: nil, sort_by: nil, sort_order: :asc) ⇒ String
Convert query results to a formatted table display.
-
#translate_pipeline_for_direct_mongodb(pipeline) ⇒ Array<Hash>
private
Apply the direct-MongoDB stage converter to every stage in a pipeline.
-
#validate_no_where_operator!(hash) ⇒ Object
deprecated
Deprecated.
Retained for backwards compatibility. Use PipelineSecurity.validate_filter! for new code.
-
#validate_pipeline!(pipeline) ⇒ Object
Validates that a pipeline does not contain dangerous operators.
-
#where(expressions = nil, opts = {}) ⇒ self
Add additional query constraints to the
whereclause. -
#where_constraints ⇒ Hash
Formats the current set of Parse::Constraint instances in the where clause as an expression hash.
-
#writable_by(permission, mongo_direct: nil) ⇒ Parse::Query
Filter by ACL write permissions using exact permission strings.
-
#writable_by_role(role_name, mongo_direct: nil) ⇒ Parse::Query
Filter by ACL write permissions using role names (adds "role:" prefix).
-
#|(other_query) ⇒ Query
The combined query with an OR clause.
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.
448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 |
# File 'lib/parse/query.rb', line 448 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_introspection ⇒ Symbol
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.
|
|
# File 'lib/parse/query.rb', line 254
|
.field_formatter ⇒ Symbol
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.
268 269 270 |
# File 'lib/parse/query.rb', line 268 def field_formatter @field_formatter end |
Instance Attribute Details
#acl_role ⇒ Parse::Role, ... (readonly)
Returns the role the query was scoped to via #scope_to_role, or nil.
1684 1685 1686 |
# File 'lib/parse/query.rb', line 1684 def acl_role @acl_role end |
#acl_user ⇒ Parse::User, ... (readonly)
Returns the user the query was scoped to via #scope_to_user, or nil for unscoped queries.
1680 1681 1682 |
# File 'lib/parse/query.rb', line 1680 def acl_user @acl_user end |
#cache ⇒ Boolean, 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.
183 |
# File 'lib/parse/query.rb', line 183 attr_reader :table, :session_token |
#client ⇒ Parse::Client
Returns the client to use for making the API request.
183 |
# File 'lib/parse/query.rb', line 183 attr_reader :table, :session_token |
#key ⇒ String
This parameter is used to support select queries where you have to
pass a key parameter for matching different tables.
183 |
# File 'lib/parse/query.rb', line 183 attr_reader :table, :session_token |
#read_preference ⇒ Symbol, String
Set the MongoDB read preference for this query. This allows directing read queries to secondary replicas for load balancing.
183 |
# File 'lib/parse/query.rb', line 183 attr_reader :table, :session_token |
#session_token ⇒ Object
Returns the value of attribute session_token.
183 |
# File 'lib/parse/query.rb', line 183 attr_reader :table, :session_token |
#table ⇒ String
Returns the name of the Parse collection to query against.
183 184 185 |
# File 'lib/parse/query.rb', line 183 def table @table end |
#use_master_key ⇒ Boolean
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.
183 |
# File 'lib/parse/query.rb', line 183 attr_reader :table, :session_token |
#verbose_aggregate ⇒ Object
Returns the value of attribute verbose_aggregate.
185 186 187 |
# File 'lib/parse/query.rb', line 185 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.
331 332 333 |
# File 'lib/parse/query.rb', line 331 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
5101 5102 5103 5104 5105 5106 5107 5108 5109 5110 5111 5112 5113 5114 5115 5116 5117 5118 5119 5120 5121 5122 5123 5124 5125 5126 5127 5128 5129 5130 5131 5132 |
# File 'lib/parse/query.rb', line 5101 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.
357 358 359 |
# File 'lib/parse/query.rb', line 357 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.
346 347 348 |
# File 'lib/parse/query.rb', line 346 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.
278 279 280 281 282 283 284 |
# File 'lib/parse/query.rb', line 278 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_classes ⇒ Object
Known Parse classes for fast validation - dynamically loaded from schema
73 74 75 76 77 78 79 80 81 82 83 84 85 |
# File 'lib/parse/query.rb', line 73 def self.known_parse_classes @known_parse_classes ||= begin # Get all classes from Parse schema response = Parse.client.schemas schema_classes = response.success? ? response.result.dig("results")&.map { |cls| cls["className"] } || [] : [] # Add built-in Parse classes built_in_classes = %w[_User _Role _Session _Installation _Audience User Role Session Installation Audience] (built_in_classes + schema_classes).uniq.freeze rescue # Fallback to built-in classes if schema query fails (e.g., during testing without server) %w[_User _Role _Session _Installation _Audience User Role Session Installation Audience].freeze end end |
.option_key?(key) ⇒ Boolean
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).
246 247 248 249 |
# File 'lib/parse/query.rb', line 246 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
5066 5067 5068 5069 5070 5071 5072 5073 5074 5075 5076 5077 5078 5079 5080 5081 5082 5083 5084 5085 5086 5087 5088 5089 5090 5091 5092 5093 5094 |
# File 'lib/parse/query.rb', line 5066 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] }
299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 |
# File 'lib/parse/query.rb', line 299 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_warned ⇒ Object
Process-wide [table, field] cache for warn-once dedup in
#handle_unresolvable_pointer_in_array!.
272 273 274 |
# File 'lib/parse/query.rb', line 272 def pointer_shape_warned @pointer_shape_warned ||= {} end |
.reset_known_parse_classes! ⇒ Object
Allow resetting the cached known classes (useful for testing)
88 89 90 |
# File 'lib/parse/query.rb', line 88 def self.reset_known_parse_classes! @known_parse_classes = nil end |
.to_snake_case(str) ⇒ String
Convert camelCase string to snake_case
289 290 291 |
# File 'lib/parse/query.rb', line 289 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.
829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 |
# File 'lib/parse/query.rb', line 829 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
807 808 809 810 811 |
# File 'lib/parse/query.rb', line 807 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
102 |
# File 'lib/parse/query.rb', line 102 define_model_callbacks :prepare, only: [:after, :before] |
#aggregate(pipeline, verbose: nil, mongo_direct: nil, rewrite_lookups: nil) ⇒ Object Also known as: aggregate_pipeline
3068 3069 3070 3071 3072 3073 3074 3075 3076 3077 3078 3079 3080 3081 3082 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 3109 3110 3111 3112 3113 3114 3115 3116 3117 3118 3119 3120 3121 3122 3123 3124 3125 3126 3127 3128 3129 3130 3131 3132 3133 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 |
# File 'lib/parse/query.rb', line 3068 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.
3274 3275 3276 3277 3278 3279 3280 3281 3282 3283 3284 3285 3286 3287 3288 3289 |
# File 'lib/parse/query.rb', line 3274 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.
3714 3715 3716 3717 3718 |
# File 'lib/parse/query.rb', line 3714 def all(expressions = { limit: :max }, &block) conditions(expressions) return results(&block) if block_given? results end |
#as_json(*args) ⇒ Hash
3840 3841 3842 |
# File 'lib/parse/query.rb', line 3840 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.
2281 2282 2283 2284 2285 2286 2287 2288 2289 2290 2291 2292 2293 2294 2295 2296 2297 2298 2299 2300 2301 2302 2303 2304 2305 2306 2307 |
# File 'lib/parse/query.rb', line 2281 def atlas_autocomplete(query, field:, **) 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" } [:filter] = ([:filter] || {}).merge(regular_constraints) if regular_constraints.any? end # Use query limit if set and no explicit limit provided [:limit] ||= (@limit.is_a?(Numeric) && @limit > 0 ? @limit : 10) [:class_name] = @table # Forward the query's read_preference (set via `#read_pref`). # See #atlas_search for the parity rationale. if @read_preference && !.key?(:read_preference) [:read_preference] = @read_preference end Parse::AtlasSearch.autocomplete(@table, query, field: field, **) 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.
2337 2338 2339 2340 2341 2342 2343 2344 2345 2346 2347 2348 2349 2350 2351 2352 2353 2354 2355 2356 2357 2358 2359 2360 2361 2362 2363 2364 |
# File 'lib/parse/query.rb', line 2337 def atlas_facets(query, facets, **) 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" } [:filter] = ([:filter] || {}).merge(regular_constraints) if regular_constraints.any? end # Use query limit/skip if set [:limit] ||= (@limit.is_a?(Numeric) && @limit > 0 ? @limit : 100) [:skip] ||= (@skip > 0 ? @skip : 0) [:class_name] = @table # Forward the query's read_preference (set via `#read_pref`). # See #atlas_search for the parity rationale. if @read_preference && !.key?(:read_preference) [:read_preference] = @read_preference end Parse::AtlasSearch.faceted_search(@table, query, facets, **) 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.
2172 2173 2174 2175 2176 2177 2178 2179 2180 2181 2182 2183 2184 2185 2186 2187 2188 2189 2190 2191 2192 2193 2194 2195 2196 2197 2198 2199 2200 2201 2202 2203 2204 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 |
# File 'lib/parse/query.rb', line 2172 def atlas_search(query = nil, **, &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 = [:limit] || (@limit.is_a?(Numeric) && @limit > 0 ? @limit : 100) skip_val = [:skip] || (@skip > 0 ? @skip : 0) if block_given? # Builder block mode index_name = [: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 [: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" } [:filter] = ([:filter] || {}).merge(regular_constraints) if regular_constraints.any? end [:class_name] = @table [:limit] = limit [: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 && !.key?(:read_preference) [:read_preference] = @read_preference end Parse::AtlasSearch.search(@table, query, **) end end |
#average(field) ⇒ Float Also known as: avg
Calculate the average of values for a specific field.
3961 3962 3963 3964 3965 3966 3967 3968 3969 3970 3971 3972 3973 3974 3975 |
# File 'lib/parse/query.rb', line 3961 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
102 |
# File 'lib/parse/query.rb', line 102 define_model_callbacks :prepare, only: [:after, :before] |
#build_aggregation_pipeline ⇒ Array
Build the complete aggregation pipeline from constraints Pipeline order: $match (regular) -> $lookup (subqueries) -> $match (post-lookup) -> $match (aggregation) -> non-$match stages -> limit/skip
3457 3458 3459 3460 3461 3462 3463 3464 3465 3466 3467 3468 3469 3470 3471 3472 3473 3474 3475 3476 3477 3478 3479 3480 3481 3482 3483 3484 3485 3486 3487 3488 3489 3490 3491 3492 3493 3494 3495 3496 3497 3498 3499 3500 3501 3502 3503 3504 3505 3506 3507 3508 3509 3510 3511 3512 3513 3514 3515 3516 3517 3518 3519 3520 3521 3522 3523 3524 3525 3526 3527 3528 3529 3530 3531 3532 3533 3534 3535 3536 3537 3538 3539 3540 3541 3542 3543 3544 3545 3546 3547 3548 3549 3550 3551 3552 3553 3554 3555 3556 3557 3558 3559 3560 3561 3562 3563 3564 3565 |
# File 'lib/parse/query.rb', line 3457 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_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.
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).
2372 2373 2374 2375 2376 2377 2378 2379 2380 2381 2382 2383 2384 2385 2386 2387 2388 2389 2390 2391 2392 2393 2394 2395 2396 2397 2398 2399 2400 2401 2402 2403 2404 2405 2406 2407 2408 2409 2410 2411 2412 2413 2414 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 |
# File 'lib/parse/query.rb', line 2372 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
3666 3667 3668 3669 3670 3671 3672 3673 3674 3675 3676 3677 3678 3679 3680 3681 3682 3683 3684 |
# File 'lib/parse/query.rb', line 3666 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.
2470 2471 2472 2473 2474 2475 2476 2477 2478 2479 2480 2481 2482 2483 2484 2485 2486 2487 2488 2489 2490 2491 2492 2493 2494 2495 2496 2497 2498 2499 2500 2501 2502 2503 2504 2505 2506 2507 2508 2509 2510 2511 2512 2513 2514 2515 2516 2517 2518 2519 2520 2521 2522 2523 2524 2525 2526 |
# File 'lib/parse/query.rb', line 2470 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
559 560 561 562 |
# File 'lib/parse/query.rb', line 559 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.
407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 |
# File 'lib/parse/query.rb', line 407 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 |
#clone ⇒ Parse::Query
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
5140 5141 5142 5143 5144 5145 5146 5147 5148 5149 5150 5151 5152 5153 5154 5155 5156 5157 5158 5159 5160 5161 5162 5163 5164 |
# File 'lib/parse/query.rb', line 5140 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.}, 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.
3858 3859 3860 3861 3862 3863 3864 3865 3866 3867 3868 3869 3870 3871 3872 3873 3874 3875 3876 3877 3878 3879 3880 3881 3882 3883 3884 3885 3886 3887 3888 3889 3890 3891 |
# File 'lib/parse/query.rb', line 3858 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_where ⇒ Hash
Returns a hash representing just the where clause of this
query, with SDK-internal routing markers stripped.
3895 3896 3897 |
# File 'lib/parse/query.rb', line 3895 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.
480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 |
# File 'lib/parse/query.rb', line 480 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
900 901 902 |
# File 'lib/parse/query.rb', line 900 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.
2852 2853 2854 2855 2856 2857 2858 2859 2860 |
# File 'lib/parse/query.rb', line 2852 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.
2564 2565 2566 2567 2568 2569 2570 2571 2572 2573 2574 2575 2576 2577 2578 2579 2580 2581 2582 2583 2584 2585 2586 2587 2588 2589 2590 |
# File 'lib/parse/query.rb', line 2564 def convert_constraints_for_direct_mongodb(constraints) return constraints unless constraints.is_a?(Hash) 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.
2596 2597 2598 2599 2600 2601 2602 2603 2604 2605 2606 2607 2608 2609 2610 2611 2612 2613 2614 2615 2616 2617 2618 2619 2620 2621 2622 2623 2624 2625 2626 2627 2628 2629 2630 2631 2632 2633 2634 2635 2636 2637 2638 2639 2640 2641 2642 2643 2644 2645 2646 2647 2648 2649 2650 2651 2652 2653 2654 2655 2656 2657 2658 2659 2660 2661 2662 2663 2664 2665 2666 2667 2668 2669 2670 2671 2672 2673 2674 2675 2676 2677 2678 2679 2680 |
# File 'lib/parse/query.rb', line 2596 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.
2836 2837 2838 2839 2840 2841 2842 2843 2844 |
# File 'lib/parse/query.rb', line 2836 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.
2780 2781 2782 2783 2784 2785 2786 2787 2788 2789 2790 2791 2792 2793 |
# File 'lib/parse/query.rb', line 2780 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.
2802 2803 2804 2805 2806 2807 2808 2809 2810 |
# File 'lib/parse/query.rb', line 2802 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.)
2867 2868 2869 2870 2871 2872 2873 2874 2875 2876 |
# File 'lib/parse/query.rb', line 2867 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.
2814 2815 2816 2817 2818 2819 2820 2821 2822 2823 |
# File 'lib/parse/query.rb', line 2814 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.
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 |
# File 'lib/parse/query.rb', line 2747 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.
2687 2688 2689 2690 2691 2692 2693 2694 2695 2696 2697 2698 2699 2700 2701 2702 2703 2704 2705 2706 2707 2708 2709 2710 2711 2712 2713 2714 2715 2716 2717 2718 2719 2720 2721 2722 2723 2724 2725 2726 2727 2728 |
# File 'lib/parse/query.rb', line 2687 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.
1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 |
# File 'lib/parse/query.rb', line 1127 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
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.
1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 |
# File 'lib/parse/query.rb', line 1980 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
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.
1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 |
# File 'lib/parse/query.rb', line 1185 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.
2945 2946 2947 |
# File 'lib/parse/query.rb', line 2945 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.
3723 3724 3725 3726 3727 3728 3729 3730 3731 3732 |
# File 'lib/parse/query.rb', line 3723 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.
3017 3018 3019 3020 3021 3022 3023 3024 3025 3026 3027 3028 3029 3030 3031 3032 3033 3034 3035 3036 3037 3038 3039 3040 3041 3042 |
# File 'lib/parse/query.rb', line 3017 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
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.
1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 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 |
# File 'lib/parse/query.rb', line 1005 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
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.
2043 2044 2045 2046 2047 2048 2049 2050 2051 2052 2053 2054 2055 2056 2057 2058 2059 2060 2061 2062 2063 2064 2065 2066 2067 2068 2069 2070 2071 2072 2073 2074 2075 2076 2077 2078 2079 2080 2081 2082 2083 2084 2085 2086 2087 2088 2089 2090 2091 2092 2093 2094 2095 2096 2097 2098 2099 2100 2101 2102 2103 2104 2105 2106 2107 2108 2109 2110 2111 2112 2113 2114 2115 2116 2117 2118 2119 |
# File 'lib/parse/query.rb', line 2043 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.
2126 2127 2128 2129 2130 2131 |
# File 'lib/parse/query.rb', line 2126 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.
4232 4233 4234 4235 4236 4237 4238 4239 |
# File 'lib/parse/query.rb', line 4232 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).
1098 1099 1100 |
# File 'lib/parse/query.rb', line 1098 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.
1587 1588 1589 1590 1591 1592 |
# File 'lib/parse/query.rb', line 1587 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
1223 1224 1225 1226 |
# File 'lib/parse/query.rb', line 1223 def each(&block) return results.enum_for(:each) unless block_given? # Sparkling magic! results.each(&block) end |
#execute_aggregation_pipeline ⇒ Aggregation
Execute an aggregation pipeline for queries with pipeline constraints
3419 3420 3421 3422 3423 3424 3425 3426 3427 3428 3429 3430 3431 3432 3433 3434 3435 3436 3437 3438 3439 3440 3441 3442 3443 |
# File 'lib/parse/query.rb', line 3419 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 |
#explain ⇒ Hash
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.
2999 3000 3001 3002 3003 3004 3005 3006 3007 3008 |
# File 'lib/parse/query.rb', line 2999 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.
3573 3574 3575 3576 3577 3578 3579 3580 3581 3582 3583 3584 3585 3586 3587 3588 3589 3590 3591 3592 3593 3594 3595 3596 3597 3598 3599 3600 3601 3602 3603 3604 3605 3606 3607 3608 3609 3610 3611 3612 3613 3614 3615 3616 3617 3618 3619 3620 3621 3622 3623 3624 3625 3626 3627 3628 3629 3630 3631 3632 3633 3634 3635 3636 3637 3638 3639 3640 3641 3642 3643 3644 3645 3646 3647 3648 3649 3650 3651 3652 3653 3654 3655 3656 3657 3658 3659 3660 3661 |
# File 'lib/parse/query.rb', line 3573 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.
1461 1462 1463 1464 1465 1466 1467 |
# File 'lib/parse/query.rb', line 1461 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
Supports all constraint options like :keys, :includes, :order, etc.
1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 |
# File 'lib/parse/query.rb', line 1259 def first(limit_or_constraints = 1, mongo_direct: false, **) # 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() unless .empty? fetch_count == 1 ? results.first : results.first(fetch_count) end |
#first_direct(limit_or_constraints = 1) ⇒ Parse::Object, ...
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.
1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 |
# File 'lib/parse/query.rb', line 1930 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.
1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 |
# File 'lib/parse/query.rb', line 1346 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.
2534 2535 2536 2537 2538 2539 2540 2541 2542 2543 2544 2545 2546 2547 2548 2549 2550 2551 2552 2553 2554 2555 2556 2557 2558 |
# File 'lib/parse/query.rb', line 2534 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.
4044 4045 4046 4047 4048 4049 4050 4051 4052 4053 4054 |
# File 'lib/parse/query.rb', line 4044 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.
4204 4205 4206 4207 4208 4209 4210 4211 4212 4213 4214 4215 4216 4217 4218 |
# File 'lib/parse/query.rb', line 4204 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.
4078 4079 4080 4081 4082 4083 4084 4085 4086 4087 4088 4089 4090 4091 4092 4093 4094 4095 4096 4097 4098 4099 4100 4101 4102 4103 4104 4105 4106 4107 4108 4109 4110 4111 4112 4113 4114 4115 4116 4117 4118 4119 4120 |
# File 'lib/parse/query.rb', line 4078 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
3689 3690 3691 3692 3693 3694 3695 3696 3697 3698 3699 3700 3701 3702 3703 |
# File 'lib/parse/query.rb', line 3689 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
800 801 802 |
# File 'lib/parse/query.rb', line 800 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.
787 788 789 790 791 792 793 794 795 796 797 |
# File 'lib/parse/query.rb', line 787 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
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.
578 579 580 581 582 583 584 585 586 587 588 |
# File 'lib/parse/query.rb', line 578 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+
Supports all constraint options like :keys, :includes, :limit, etc.
Returns the most recently updated object(s) (ordered by updated_at descending).
1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 |
# File 'lib/parse/query.rb', line 1330 def last_updated(limit = 1, **) # Allow limit to be overridden via options limit = .delete(:limit) if .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() unless .empty? limit == 1 ? results.first : results.first(limit) end |
#latest(limit = 1, **options) ⇒ Parse::Object+
Supports all constraint options like :keys, :includes, :limit, etc.
Returns the most recently created object(s) (ordered by created_at descending).
1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 |
# File 'lib/parse/query.rb', line 1308 def latest(limit = 1, **) # Allow limit to be overridden via options limit = .delete(:limit) if .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() unless .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.
730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 |
# File 'lib/parse/query.rb', line 730 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
1231 1232 1233 1234 |
# File 'lib/parse/query.rb', line 1231 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.
4001 4002 4003 4004 4005 4006 4007 4008 4009 4010 4011 4012 4013 4014 4015 |
# File 'lib/parse/query.rb', line 4001 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.
3982 3983 3984 3985 3986 3987 3988 3989 3990 3991 3992 3993 3994 3995 3996 |
# File 'lib/parse/query.rb', line 3982 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 "*".
5322 5323 5324 5325 5326 |
# File 'lib/parse/query.rb', line 5322 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 "*".
5335 5336 5337 5338 5339 |
# File 'lib/parse/query.rb', line 5335 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.
950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 |
# File 'lib/parse/query.rb', line 950 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.
651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 |
# File 'lib/parse/query.rb', line 651 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 |
#pipeline ⇒ Array
Returns the aggregation pipeline for this query if it contains pipeline-based constraints
3911 3912 3913 3914 3915 3916 3917 3918 3919 3920 3921 3922 3923 3924 3925 |
# File 'lib/parse/query.rb', line 3911 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
3448 3449 3450 3451 3452 |
# File 'lib/parse/query.rb', line 3448 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.
609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 |
# File 'lib/parse/query.rb', line 609 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.
3848 3849 3850 |
# File 'lib/parse/query.rb', line 3848 def prepared(includeClassName: false) compile(encode: false, includeClassName: includeClassName) end |
#pretty ⇒ String
Retruns a formatted JSON string representing the query, useful for debugging.
3935 3936 3937 |
# File 'lib/parse/query.rb', line 3935 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.
5308 5309 5310 5311 |
# File 'lib/parse/query.rb', line 5308 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.
5280 5281 5282 |
# File 'lib/parse/query.rb', line 5280 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.
5294 5295 5296 |
# File 'lib/parse/query.rb', line 5294 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 "*".
5256 5257 5258 |
# File 'lib/parse/query.rb', line 5256 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.
5268 5269 5270 |
# File 'lib/parse/query.rb', line 5268 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)
1816 1817 1818 |
# File 'lib/parse/query.rb', line 1816 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.
763 764 765 766 |
# File 'lib/parse/query.rb', line 763 def read_pref(preference) @read_preference = preference self end |
#readable_by(permission, mongo_direct: nil) ⇒ Parse::Query
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.
5184 5185 5186 5187 5188 |
# File 'lib/parse/query.rb', line 5184 def readable_by(, mongo_direct: nil) @acl_query_mongo_direct = mongo_direct unless mongo_direct.nil? where(:ACL.readable_by => ) self end |
#readable_by_role(role_name, mongo_direct: nil) ⇒ Parse::Query
Filter by ACL read permissions using role names (adds "role:" prefix).
5199 5200 5201 5202 5203 |
# File 'lib/parse/query.rb', line 5199 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 |
#related_to(field, pointer) ⇒ Object
768 769 770 771 772 |
# File 'lib/parse/query.rb', line 768 def (field, pointer) raise ArgumentError, "Object value must be a Parse::Pointer type" unless pointer.is_a?(Parse::Pointer) add_constraint field.to_sym., pointer self #chaining end |
#requires_aggregation? ⇒ Boolean
Check if this query requires aggregation pipeline execution
3929 3930 3931 |
# File 'lib/parse/query.rb', line 3929 def requires_aggregation? !pipeline.empty? end |
#requires_aggregation_pipeline? ⇒ Boolean
Check if this query contains constraints that require aggregation pipeline processing
1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 |
# File 'lib/parse/query.rb', line 1797 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.
1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 |
# File 'lib/parse/query.rb', line 1570 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
1824 1825 1826 |
# File 'lib/parse/query.rb', line 1824 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.
1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 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 |
# File 'lib/parse/query.rb', line 1494 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>
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.
1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 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 |
# File 'lib/parse/query.rb', line 1853 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 aletvariable 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
$literalis a string constant, not a field reference, and must not be rewritten.
2893 2894 2895 2896 2897 2898 2899 2900 2901 2902 2903 2904 2905 2906 2907 2908 2909 2910 2911 2912 2913 2914 2915 2916 2917 |
# File 'lib/parse/query.rb', line 2893 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.
1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 |
# File 'lib/parse/query.rb', line 1666 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.
1631 1632 1633 1634 1635 1636 |
# File 'lib/parse/query.rb', line 1631 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
1239 1240 1241 1242 |
# File 'lib/parse/query.rb', line 1239 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.
692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 |
# File 'lib/parse/query.rb', line 692 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) ⇒ 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.
2971 2972 2973 2974 2975 2976 2977 2978 2979 2980 2981 |
# File 'lib/parse/query.rb', line 2971 def subscribe(fields: nil, session_token: nil, client: nil) 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, ) end |
#sum(field) ⇒ Numeric
Calculate the sum of values for a specific field.
3942 3943 3944 3945 3946 3947 3948 3949 3950 3951 3952 3953 3954 3955 3956 |
# File 'lib/parse/query.rb', line 3942 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_pointers(list, field = nil) ⇒ Array<Parse::Pointer>
Builds Parse::Pointer objects based on the set of Parse JSON hashes in an array.
3802 3803 3804 3805 3806 3807 3808 3809 3810 3811 3812 3813 3814 3815 3816 3817 3818 3819 3820 3821 3822 3823 3824 3825 3826 3827 3828 3829 3830 3831 3832 3833 3834 3835 3836 3837 |
# File 'lib/parse/query.rb', line 3802 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.
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 |
# File 'lib/parse/query.rb', line 4157 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).
3221 3222 3223 3224 |
# File 'lib/parse/query.rb', line 3221 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
Retained for backwards compatibility. Use PipelineSecurity.validate_filter! for new code.
3252 3253 3254 3255 3256 |
# File 'lib/parse/query.rb', line 3252 def validate_no_where_operator!(hash) Parse::PipelineSecurity.validate_filter!(hash) rescue Parse::PipelineSecurity::Error => e raise ArgumentError, e. end |
#validate_pipeline!(pipeline) ⇒ Object
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.
3242 3243 3244 3245 3246 |
# File 'lib/parse/query.rb', line 3242 def validate_pipeline!(pipeline) Parse::PipelineSecurity.validate_filter!(pipeline) rescue Parse::PipelineSecurity::Error => e raise ArgumentError, e. 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.
925 926 927 928 929 930 931 932 |
# File 'lib/parse/query.rb', line 925 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_constraints ⇒ Hash
Formats the current set of Parse::Constraint instances in the where clause as an expression hash.
907 908 909 |
# File 'lib/parse/query.rb', line 907 def where_constraints @where.reduce({}) { |memo, constraint| memo[constraint.operation] = constraint.value; memo } end |
#writable_by(permission, mongo_direct: nil) ⇒ Parse::Query
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.
5223 5224 5225 5226 5227 |
# File 'lib/parse/query.rb', line 5223 def writable_by(, mongo_direct: nil) @acl_query_mongo_direct = mongo_direct unless mongo_direct.nil? where(:ACL.writable_by => ) self end |
#writable_by_role(role_name, mongo_direct: nil) ⇒ Parse::Query
Filter by ACL write permissions using role names (adds "role:" prefix).
5238 5239 5240 5241 5242 |
# File 'lib/parse/query.rb', line 5238 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.
980 981 982 983 984 985 |
# File 'lib/parse/query.rb', line 980 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 |