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
- BUILT_IN_PARSE_CLASSES =
Built-in Parse classes always considered known, independent of the server schema. Used both as the seed for the dynamic list and as the transient fallback when the schema fetch fails.
%w[ _User _Role _Session _Installation _Audience User Role Session Installation Audience ].freeze
- QUERY_OPTION_KEYS =
The set of symbol keys that #conditions treats as query-shape options (cache TTL, ordering, limits, ACL convenience helpers, session/master-key overrides) rather than as field-name constraints. External callers that need to partition a user-supplied constraints Hash into "real constraints vs query options" — most notably
Parse::Object.first_or_create!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, use_master_key: false) {|subscription| ... } ⇒ 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.
475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 |
# File 'lib/parse/query.rb', line 475 def initialize(table, constraints = {}) table = table.to_s.to_parse_class if table.is_a?(Symbol) table = table.parse_class if table.respond_to?(:parse_class) raise ArgumentError, "First parameter should be the name of the Parse class (table)" unless table.is_a?(String) @count = 0 #non-zero/1 implies a count query request @where = [] @order = [] @keys = [] @includes = [] @limit = nil @skip = 0 @table = table @cache = Parse.default_query_cache # Tri-state: `nil` means "no caller preference" — the request layer # then applies the master-key default, the `Parse.client_mode` flag, # and the `Parse.with_session` ambient as configured. Explicit # `true` / `false` (set via `use_master_key=` or the `use_master_key:` # constraint key) wins over both. A `true` default here would # silently smuggle the master-key header past every client-mode # query, so we deliberately leave the decision to the request layer # unless the caller said otherwise. @use_master_key = nil @verbose_aggregate = false conditions constraints end |
Class Attribute Details
.allow_scope_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 281
|
.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.
295 296 297 |
# File 'lib/parse/query.rb', line 295 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.
1711 1712 1713 |
# File 'lib/parse/query.rb', line 1711 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.
1707 1708 1709 |
# File 'lib/parse/query.rb', line 1707 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.
210 |
# File 'lib/parse/query.rb', line 210 attr_reader :table, :session_token |
#client ⇒ Parse::Client
Returns the client to use for making the API request.
210 |
# File 'lib/parse/query.rb', line 210 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.
210 |
# File 'lib/parse/query.rb', line 210 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.
210 |
# File 'lib/parse/query.rb', line 210 attr_reader :table, :session_token |
#session_token ⇒ Object
Returns the value of attribute session_token.
210 |
# File 'lib/parse/query.rb', line 210 attr_reader :table, :session_token |
#table ⇒ String
Returns the name of the Parse collection to query against.
210 211 212 |
# File 'lib/parse/query.rb', line 210 def table @table end |
#use_master_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.
210 |
# File 'lib/parse/query.rb', line 210 attr_reader :table, :session_token |
#verbose_aggregate ⇒ Object
Returns the value of attribute verbose_aggregate.
212 213 214 |
# File 'lib/parse/query.rb', line 212 def verbose_aggregate @verbose_aggregate end |
Class Method Details
.all(table, constraints = { limit: :max }) ⇒ Query
Helper method to create a query with constraints for a specific Parse collection.
Also sets the default limit count to :max.
358 359 360 |
# File 'lib/parse/query.rb', line 358 def all(table, constraints = { limit: :max }) self.new(table, constraints.reverse_merge({ limit: :max })) end |
.and(*queries) ⇒ Parse::Query
Combines multiple queries with AND logic using full pipeline approach Each query's complete constraint set is ANDed together
5167 5168 5169 5170 5171 5172 5173 5174 5175 5176 5177 5178 5179 5180 5181 5182 5183 5184 5185 5186 5187 5188 5189 5190 5191 5192 5193 5194 5195 5196 5197 5198 |
# File 'lib/parse/query.rb', line 5167 def self.and(*queries) queries = queries.flatten.compact return nil if queries.empty? # Get the table from the first query table = queries.first.table # Ensure all queries are for the same table unless queries.all? { |q| q.table == table } raise ArgumentError, "All queries passed to Parse::Query.and must be for the same Parse class." end # Start with an empty query for this table result = self.new(table) # Filter to only queries that have constraints queries = queries.filter { |q| q.where.present? && !q.where.empty? } # Add each query's complete constraint set with AND logic # Multiple constraints in a query are implicitly ANDed together by Parse queries.each do |query| # Compile the where constraints to check if they result in empty conditions compiled_where = Parse::Query.compile_where(query.where) unless compiled_where.empty? # Directly append constraints to result's where array # (where method only accepts Hash, but query.where returns Array<Constraint>) result.instance_variable_get(:@where).concat(query.where) end end result end |
.compile_markers(where) ⇒ Hash
Return the un-stripped reduced hash so the routing/pipeline layer
can inspect __-prefixed markers (e.g. "__mongo_direct_only",
"__aggregation_pipeline"). These markers are SDK-internal hints
and must never be sent to Parse REST or MongoDB — that's what
compile_where is for.
384 385 386 |
# File 'lib/parse/query.rb', line 384 def compile_markers(where) constraint_reduce(where) end |
.compile_where(where) ⇒ Hash
This methods takes a set of constraints and merges them to build a final
where constraint clause for sending to the Parse backend.
__-prefixed internal routing markers (e.g. "__mongo_direct_only"
and "__aggregation_pipeline") are stripped from the returned hash —
they are SDK-internal hints that must never reach Parse REST or
MongoDB. Use compile_markers (instance method #compile_markers)
to retrieve them for routing decisions / pipeline assembly.
373 374 375 |
# File 'lib/parse/query.rb', line 373 def compile_where(where) constraint_reduce(where).reject { |k, _| k.is_a?(String) && k.start_with?("__") } end |
.format_field(str) ⇒ String
Returns formatted string using field_formatter.
305 306 307 308 309 310 311 |
# File 'lib/parse/query.rb', line 305 def format_field(str) res = str.to_s.strip if field_formatter.present? && res.respond_to?(field_formatter) res = res.send(field_formatter) end res end |
.known_parse_classes ⇒ Object
Known Parse classes for fast validation - dynamically loaded from schema.
The successful result is memoized; a failed schema fetch is NOT cached — it returns the built-in fallback for this call only, so a transient server outage during boot doesn't permanently strip every application- defined class from the known set (which would make class-accessibility checks reject custom classes for the process lifetime). The narrowed rescue logs the failure instead of swallowing it silently.
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
# File 'lib/parse/query.rb', line 92 def self.known_parse_classes cached = @known_parse_classes return cached if cached @known_parse_classes_mutex.synchronize do # Re-check under the lock: a racing caller may have populated it. return @known_parse_classes if @known_parse_classes begin response = Parse.client.schemas schema_classes = response.success? ? response.results.map { |cls| cls["className"] } : [] @known_parse_classes = (BUILT_IN_PARSE_CLASSES + schema_classes).uniq.freeze rescue Parse::Error, Faraday::Error => e # Don't cache the fallback — let the next call retry the fetch once # the server is reachable again. warn "[Parse::Query] schema fetch failed (#{e.class}: #{e.}); " \ "falling back to built-in classes for this check only." BUILT_IN_PARSE_CLASSES end 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).
273 274 275 276 |
# File 'lib/parse/query.rb', line 273 def option_key?(key) return false unless key.is_a?(Symbol) || key.is_a?(String) QUERY_OPTION_KEYS.include?(key.to_sym) end |
.or(*queries) ⇒ Parse::Query
Combines multiple queries with OR logic using full pipeline approach Each query's complete constraint set becomes one branch of the OR condition
5132 5133 5134 5135 5136 5137 5138 5139 5140 5141 5142 5143 5144 5145 5146 5147 5148 5149 5150 5151 5152 5153 5154 5155 5156 5157 5158 5159 5160 |
# File 'lib/parse/query.rb', line 5132 def self.or(*queries) queries = queries.flatten.compact return nil if queries.empty? # Get the table from the first query table = queries.first.table # Ensure all queries are for the same table unless queries.all? { |q| q.table == table } raise ArgumentError, "All queries passed to Parse::Query.or must be for the same Parse class." end # Start with an empty query for this table result = self.new(table) # Filter to only queries that have constraints queries = queries.filter { |q| q.where.present? && !q.where.empty? } # Add each query's complete constraint set as an OR branch queries.each do |query| # Compile the where constraints to check if they result in empty conditions compiled_where = Parse::Query.compile_where(query.where) unless compiled_where.empty? result.or_where(query.where) end end result end |
.parse_keys_to_nested_keys(keys) ⇒ Hash
Parses keys patterns to build a map of nested fetched keys. Handles arbitrary nesting depth (e.g., "a.b.c.d" creates entries for a, b, c). For example, ["project.name", "project.status", "author.email"] becomes: { project: [:name, :status], author: [:email] }
326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 |
# File 'lib/parse/query.rb', line 326 def parse_keys_to_nested_keys(keys) return {} if keys.nil? || keys.empty? nested_map = {} keys.each do |key_path| parts = key_path.to_s.split(".") # Skip keys without dots - they're top-level fields, not nested next if parts.length < 2 # Process each level of nesting # For path "a.b.c.d": a gets b, b gets c, c gets d parts.each_with_index do |part, index| field_name = part.to_sym nested_map[field_name] ||= [] # If there's a next part, add it to this field's nested keys if index < parts.length - 1 next_field = parts[index + 1].to_sym nested_map[field_name] << next_field unless nested_map[field_name].include?(next_field) end end end nested_map end |
.pointer_shape_warned ⇒ Object
Process-wide [table, field] cache for warn-once dedup in
#handle_unresolvable_pointer_in_array!.
299 300 301 |
# File 'lib/parse/query.rb', line 299 def pointer_shape_warned @pointer_shape_warned ||= {} end |
.reset_known_parse_classes! ⇒ Object
Allow resetting the cached known classes (useful for testing)
115 116 117 |
# File 'lib/parse/query.rb', line 115 def self.reset_known_parse_classes! @known_parse_classes = nil end |
.to_snake_case(str) ⇒ String
Convert camelCase string to snake_case
316 317 318 |
# File 'lib/parse/query.rb', line 316 def to_snake_case(str) str.to_s.underscore end |
Instance Method Details
#add_constraint(operator, value = nil, opts = {}) ⇒ self
Add a constraint to the query. This is mainly used internally for compiling constraints.
856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 |
# File 'lib/parse/query.rb', line 856 def add_constraint(operator, value = nil, opts = {}) @where ||= [] constraint = operator # assume Parse::Constraint unless constraint.is_a?(Parse::Constraint) constraint = Parse::Constraint.create(operator, value) end return unless constraint.is_a?(Parse::Constraint) # to support select queries where you have to pass a `key` parameter for matching # different tables. if constraint.operand == :key || constraint.operand == "key" @key = constraint.value return end unless opts[:filter] == false constraint.operand = Query.format_field(constraint.operand) end reject_vector_constraint!(constraint) @where.push constraint @results = nil self #chaining end |
#add_constraints(list) ⇒ self
Combine a list of Constraint objects
834 835 836 837 838 |
# File 'lib/parse/query.rb', line 834 def add_constraints(list) list = Array.wrap(list).select { |m| m.is_a?(Parse::Constraint) } @where = @where + list self end |
#after_prepare { ... } ⇒ Object
A callback called after the query is compiled
129 |
# File 'lib/parse/query.rb', line 129 define_model_callbacks :prepare, only: [:after, :before] |
#aggregate(pipeline, verbose: nil, mongo_direct: nil, rewrite_lookups: nil) ⇒ Object Also known as: aggregate_pipeline
3134 3135 3136 3137 3138 3139 3140 3141 3142 3143 3144 3145 3146 3147 3148 3149 3150 3151 3152 3153 3154 3155 3156 3157 3158 3159 3160 3161 3162 3163 3164 3165 3166 3167 3168 3169 3170 3171 3172 3173 3174 3175 3176 3177 3178 3179 3180 3181 3182 3183 3184 3185 3186 3187 3188 3189 3190 3191 3192 3193 3194 3195 3196 3197 3198 3199 3200 3201 3202 3203 3204 3205 3206 3207 3208 3209 3210 3211 3212 3213 3214 3215 3216 3217 3218 3219 3220 3221 3222 3223 3224 3225 3226 3227 3228 3229 3230 3231 3232 3233 3234 3235 3236 3237 3238 3239 3240 3241 3242 3243 3244 3245 3246 3247 3248 3249 3250 3251 3252 3253 3254 3255 3256 3257 3258 3259 3260 3261 3262 3263 3264 3265 3266 3267 3268 3269 3270 3271 3272 3273 3274 3275 3276 3277 3278 3279 |
# File 'lib/parse/query.rb', line 3134 def aggregate(pipeline, verbose: nil, mongo_direct: nil, rewrite_lookups: nil) validate_pipeline!(pipeline) # Auto-rewrite LLM-style $lookup stages against logical Parse class # names into the Parse-on-Mongo column form (_p_*/parseReference) when # the foreign class declares parse_reference. Idempotent on already- # rewritten input. Controlled by Parse.rewrite_lookups (default true) # or the per-call `rewrite_lookups:` kwarg. pipeline = Parse::LookupRewriter.auto_rewrite( pipeline, class_name: @table, enabled: rewrite_lookups, ) # Automatically prepend query constraints as pipeline stages complete_pipeline = [] lookup_stages = [] # Track if we have $inQuery constraints # Add $match stage from where constraints if any exist unless @where.empty? # `compile_where` is marker-free; `compile_markers` carries the # __aggregation_pipeline stages we need to extract below. where_clause = compile_where markers = compile_markers if where_clause.any? || markers.key?("__aggregation_pipeline") # Collect match conditions and stages initial_match_conditions = [] aggregation_match_conditions = [] non_match_stages = [] post_lookup_match = {} # `where_clause` is already marker-free; treat as regular constraints. regular_constraints = where_clause if regular_constraints.any? # Handle dates first date_converted = convert_dates_for_aggregation(regular_constraints) # Extract $inQuery/$notInQuery and convert to $lookup stages if has_subquery_constraints?(date_converted) lookup_result = extract_subquery_to_lookup_stages(date_converted) date_converted = lookup_result[:constraints] lookup_stages = lookup_result[:lookup_stages] post_lookup_match = lookup_result[:post_lookup_match] end # Convert field names for aggregation context and handle pointers if date_converted.any? match_stage = convert_constraints_for_aggregation(date_converted) initial_match_conditions << match_stage end end # Extract aggregation pipeline stages from the marker view. if markers.key?("__aggregation_pipeline") markers["__aggregation_pipeline"].each do |stage| if stage.is_a?(Hash) && stage.key?("$match") aggregation_match_conditions << stage["$match"] else non_match_stages << stage end end end # Stage 1: Initial $match with regular constraints if initial_match_conditions.any? if initial_match_conditions.length == 1 complete_pipeline << { "$match" => initial_match_conditions.first } else complete_pipeline << { "$match" => { "$and" => initial_match_conditions } } end end # Stage 2: $lookup stages for subqueries ($addFields, $lookup) if lookup_stages.any? lookup_stages.each do |stage| next if stage.key?("$project") complete_pipeline << stage end # Stage 3: Post-lookup $match if post_lookup_match.any? complete_pipeline << { "$match" => post_lookup_match } end # Note: Skip cleanup $project stage - see build_aggregation_pipeline for reasoning end # Stage 5: Aggregation $match conditions if aggregation_match_conditions.any? if aggregation_match_conditions.length == 1 complete_pipeline << { "$match" => aggregation_match_conditions.first } else complete_pipeline << { "$match" => { "$and" => aggregation_match_conditions } } end end # Stage 6: Non-$match stages from aggregation pipeline complete_pipeline.concat(non_match_stages) end end # Append the provided pipeline stages complete_pipeline.concat(pipeline) # Add $sort stage from order constraints if any exist unless @order.empty? sort_stage = {} @order.each do |order_obj| # order_obj is a Parse::Order object with field and direction field_name = order_obj.field.to_s direction = order_obj.direction == :desc ? -1 : 1 sort_stage[field_name] = direction end complete_pipeline << { "$sort" => sort_stage } if sort_stage.any? end # Add $skip stage if specified if @skip > 0 complete_pipeline << { "$skip" => @skip } end # Add $limit stage if specified if @limit.is_a?(Numeric) && @limit > 0 complete_pipeline << { "$limit" => @limit } end # Auto-detect if mongo_direct is needed (when $inQuery constraints are present and MongoDB is available) use_mongo_direct = mongo_direct if use_mongo_direct.nil? && lookup_stages && lookup_stages.any? && defined?(Parse::MongoDB) && Parse::MongoDB.enabled? use_mongo_direct = true end # Optimize pipeline by merging consecutive $match stages complete_pipeline = deduplicate_consecutive_match_stages(complete_pipeline) # When the pipeline is bound for direct MongoDB, translate every stage # through the direct-MongoDB field rewriter so user-supplied stages # (which use logical Parse field names like `$author`) reach the # correct on-disk columns (`$_p_author`). The Parse Server route does # not need this — Parse Server applies its own translation on the # aggregate endpoint — so the rewrite is gated on use_mongo_direct. if use_mongo_direct complete_pipeline = translate_pipeline_for_direct_mongodb(complete_pipeline) end Aggregation.new(self, complete_pipeline, verbose: verbose, mongo_direct: use_mongo_direct || false) end |
#aggregate_from_query(additional_stages = [], verbose: nil, mongo_direct: nil) ⇒ Aggregation
Converts the current query into an aggregate pipeline and executes it. This method automatically converts all query constraints (where, order, limit, skip, etc.) into MongoDB aggregation pipeline stages.
3340 3341 3342 3343 3344 3345 3346 3347 3348 3349 3350 3351 3352 3353 3354 3355 |
# File 'lib/parse/query.rb', line 3340 def aggregate_from_query(additional_stages = [], verbose: nil, mongo_direct: nil) # Build pipeline from current query constraints pipeline, has_lookup_stages = build_query_aggregate_pipeline # Append any additional stages pipeline.concat(additional_stages) if additional_stages.any? # Auto-detect if mongo_direct is needed (when $inQuery constraints are present and MongoDB is available) use_mongo_direct = mongo_direct if use_mongo_direct.nil? && has_lookup_stages && defined?(Parse::MongoDB) && Parse::MongoDB.enabled? use_mongo_direct = true end # Create Aggregation directly to avoid double-applying constraints Aggregation.new(self, pipeline, verbose: verbose, mongo_direct: use_mongo_direct || false) end |
#all(expressions = { limit: :max }) { ... } ⇒ Array<Hash>, Array<Parse::Object>
Similar to #results but takes an additional set of conditions to apply. This method helps support the use of class and instance level scopes.
3780 3781 3782 3783 3784 |
# File 'lib/parse/query.rb', line 3780 def all(expressions = { limit: :max }, &block) conditions(expressions) return results(&block) if block_given? results end |
#as_json(*args) ⇒ Hash
3906 3907 3908 |
# File 'lib/parse/query.rb', line 3906 def as_json(*args) compile.as_json end |
#atlas_autocomplete(query, field:, **options) ⇒ Parse::AtlasSearch::AutocompleteResult
Execute an autocomplete search using MongoDB Atlas Search. Provides search-as-you-type functionality for a specific field.
2308 2309 2310 2311 2312 2313 2314 2315 2316 2317 2318 2319 2320 2321 2322 2323 2324 2325 2326 2327 2328 2329 2330 2331 2332 2333 2334 |
# File 'lib/parse/query.rb', line 2308 def atlas_autocomplete(query, field:, **) 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.
2364 2365 2366 2367 2368 2369 2370 2371 2372 2373 2374 2375 2376 2377 2378 2379 2380 2381 2382 2383 2384 2385 2386 2387 2388 2389 2390 2391 |
# File 'lib/parse/query.rb', line 2364 def atlas_facets(query, facets, **) 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.
2199 2200 2201 2202 2203 2204 2205 2206 2207 2208 2209 2210 2211 2212 2213 2214 2215 2216 2217 2218 2219 2220 2221 2222 2223 2224 2225 2226 2227 2228 2229 2230 2231 2232 2233 2234 2235 2236 2237 2238 2239 2240 2241 2242 2243 2244 2245 2246 2247 2248 2249 2250 2251 2252 2253 2254 2255 2256 2257 2258 2259 2260 2261 2262 2263 2264 2265 2266 2267 2268 2269 2270 2271 2272 2273 2274 2275 2276 2277 2278 |
# File 'lib/parse/query.rb', line 2199 def atlas_search(query = nil, **, &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.
4027 4028 4029 4030 4031 4032 4033 4034 4035 4036 4037 4038 4039 4040 4041 |
# File 'lib/parse/query.rb', line 4027 def average(field) if field.nil? || !field.respond_to?(:to_s) raise ArgumentError, "Invalid field name passed to `average`." end # Format field name according to Parse conventions formatted_field = format_aggregation_field(field) # Build the aggregation pipeline pipeline = [ { "$group" => { "_id" => nil, "avg" => { "$avg" => "$#{formatted_field}" } } }, ] execute_basic_aggregation(pipeline, "average", field, "avg") end |
#before_prepare { ... } ⇒ Object
A callback called before the query is compiled
129 |
# File 'lib/parse/query.rb', line 129 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
3523 3524 3525 3526 3527 3528 3529 3530 3531 3532 3533 3534 3535 3536 3537 3538 3539 3540 3541 3542 3543 3544 3545 3546 3547 3548 3549 3550 3551 3552 3553 3554 3555 3556 3557 3558 3559 3560 3561 3562 3563 3564 3565 3566 3567 3568 3569 3570 3571 3572 3573 3574 3575 3576 3577 3578 3579 3580 3581 3582 3583 3584 3585 3586 3587 3588 3589 3590 3591 3592 3593 3594 3595 3596 3597 3598 3599 3600 3601 3602 3603 3604 3605 3606 3607 3608 3609 3610 3611 3612 3613 3614 3615 3616 3617 3618 3619 3620 3621 3622 3623 3624 3625 3626 3627 3628 3629 3630 3631 |
# File 'lib/parse/query.rb', line 3523 def build_aggregation_pipeline pipeline = [] # `compile_where` is already marker-free; `compile_markers` retains # the __aggregation_pipeline marker we need to extract stages from. compiled_where = compile_where markers = compile_markers has_lookup_stages = false # Collect match conditions and stages initial_match_conditions = [] aggregation_match_conditions = [] non_match_stages = [] lookup_stages = [] post_lookup_match = {} # `compiled_where` is already marker-free; use as-is. regular_constraints = compiled_where # Process regular constraints if regular_constraints.any? # Convert symbols to strings and handle date objects for MongoDB aggregation stringified_constraints = convert_dates_for_aggregation(JSON.parse(regular_constraints.to_json)) # Extract $inQuery/$notInQuery and convert to $lookup stages if has_subquery_constraints?(stringified_constraints) lookup_result = extract_subquery_to_lookup_stages(stringified_constraints) stringified_constraints = lookup_result[:constraints] lookup_stages = lookup_result[:lookup_stages] post_lookup_match = lookup_result[:post_lookup_match] has_lookup_stages = lookup_stages.any? end # Convert remaining pointer field names and values to MongoDB aggregation format if stringified_constraints.any? stringified_constraints = convert_constraints_for_aggregation(stringified_constraints) initial_match_conditions << stringified_constraints end end # Extract aggregation pipeline stages (from empty_or_nil, set_equals, etc.) if markers.key?("__aggregation_pipeline") markers["__aggregation_pipeline"].each do |stage| if stage.is_a?(Hash) && stage.key?("$match") # Aggregation $match conditions go after lookup aggregation_match_conditions << stage["$match"] else # Non-$match stages go directly to pipeline non_match_stages << stage end end end # Stage 1: Initial $match with regular constraints (before lookup) # This filters down the dataset before the expensive $lookup if initial_match_conditions.any? if initial_match_conditions.length == 1 pipeline << { "$match" => initial_match_conditions.first } else pipeline << { "$match" => { "$and" => initial_match_conditions } } end end # Stage 2: $lookup stages for subqueries ($addFields, $lookup) # These join with related collections and filter based on subquery conditions if lookup_stages.any? # Add $addFields and $lookup stages (skip $project stages) lookup_stages.each do |stage| next if stage.key?("$project") pipeline << stage end # Stage 3: Post-lookup $match to filter based on lookup results if post_lookup_match.any? pipeline << { "$match" => post_lookup_match } end # Note: We intentionally skip cleanup $project stage because: # 1. Parse Server's aggregation result processing ignores unknown fields # 2. Using $project with exclusions can cause issues in some MongoDB versions # 3. The temporary lookup fields (_lookup_*_id, _lookup_*_result) won't affect the output end # Stage 5: Aggregation $match conditions (from empty_or_nil, set_equals, etc.) if aggregation_match_conditions.any? if aggregation_match_conditions.length == 1 pipeline << { "$match" => aggregation_match_conditions.first } else pipeline << { "$match" => { "$and" => aggregation_match_conditions } } end end # Stage 6: Non-$match stages from aggregation pipeline pipeline.concat(non_match_stages) # Stage 7: Add limit if specified if @limit.is_a?(Numeric) && @limit > 0 pipeline << { "$limit" => @limit } end # Stage 8: Add skip if specified if @skip > 0 pipeline << { "$skip" => @skip } end # Optimize pipeline by merging consecutive $match stages pipeline = deduplicate_consecutive_match_stages(pipeline) [pipeline, has_lookup_stages] end |
#build_direct_mongodb_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).
2399 2400 2401 2402 2403 2404 2405 2406 2407 2408 2409 2410 2411 2412 2413 2414 2415 2416 2417 2418 2419 2420 2421 2422 2423 2424 2425 2426 2427 2428 2429 2430 2431 2432 2433 2434 2435 2436 2437 2438 2439 2440 2441 2442 2443 2444 2445 2446 2447 2448 2449 2450 2451 2452 2453 2454 2455 2456 2457 2458 2459 2460 2461 2462 2463 2464 2465 2466 2467 2468 2469 2470 2471 2472 2473 2474 2475 2476 2477 2478 2479 2480 2481 2482 2483 2484 2485 2486 2487 2488 2489 |
# File 'lib/parse/query.rb', line 2399 def build_direct_mongodb_pipeline pipeline = [] # Mirror the REST compile() behavior: ensure each top-level included field # is also in @keys so the $project stage below does not strip the pointer # that the $lookup stage is supposed to expand. merge_includes_into_keys! # Compile the where clause and convert for direct MongoDB access. # `compile_where` already strips `__`-prefixed routing markers; use # `compile_markers` to recover the unfiltered hash for the # __aggregation_pipeline extraction below. compiled_where = compile_where markers = compile_markers # Note: the `_rperm` injection for scope_to_user no longer # happens here. It moved to Parse::MongoDB.aggregate via the # acl_user: kwarg so the same three-layer ACL simulation # (top-level $match + $lookup rewriter + post-fetch redactor) # runs for scope_to_user, session_token, and the public-only # fallback alike. See {#mongo_direct_auth_kwargs}. if compiled_where.present? # Convert field names and values for direct MongoDB access. # `compiled_where` is already marker-free, so no further # reject pass is required. mongo_constraints = convert_constraints_for_direct_mongodb(compiled_where) pipeline << { "$match" => mongo_constraints } if mongo_constraints.any? end # Handle aggregation pipeline stages (from empty_or_nil, set_equals, etc.) if markers.key?("__aggregation_pipeline") markers["__aggregation_pipeline"].each do |stage| pipeline << convert_stage_for_direct_mongodb(stage) end end # Add sort stage if order is specified if @order.any? sort_spec = {} @order.each do |order_clause| # Handle both Parse::Order objects and string representations if order_clause.is_a?(Parse::Order) field = order_clause.field.to_s direction = order_clause.direction == :desc ? -1 : 1 sort_spec[convert_field_for_direct_mongodb(field)] = direction elsif order_clause.is_a?(String) # Parse order clause (e.g., "-createdAt" or "name") if order_clause.start_with?("-") field = order_clause[1..-1] sort_spec[convert_field_for_direct_mongodb(field)] = -1 else sort_spec[convert_field_for_direct_mongodb(order_clause)] = 1 end end end pipeline << { "$sort" => sort_spec } if sort_spec.any? end # Add include/eager loading $lookup stages if @includes is populated # These stages resolve pointer fields to full objects if @includes.any? include_stages = build_include_lookup_stages(@includes) pipeline.concat(include_stages) end # Add skip stage if specified pipeline << { "$skip" => @skip } if @skip > 0 # Add limit stage if specified pipeline << { "$limit" => @limit } if @limit.is_a?(Numeric) && @limit > 0 # Add $project stage if specific keys are requested # Always include required fields: _id, _created_at, _updated_at, _acl if @keys.any? project_stage = { "_id" => 1, "_created_at" => 1, "_updated_at" => 1, "_acl" => 1, } @keys.each do |key| mongo_field = convert_field_for_direct_mongodb(key.to_s) project_stage[mongo_field] = 1 end pipeline << { "$project" => project_stage } end # Optimize pipeline by merging consecutive $match stages deduplicate_consecutive_match_stages(pipeline) end |
#build_filter_condition(where) ⇒ Hash
Build a $filter condition expression from where constraints
3732 3733 3734 3735 3736 3737 3738 3739 3740 3741 3742 3743 3744 3745 3746 3747 3748 3749 3750 |
# File 'lib/parse/query.rb', line 3732 def build_filter_condition(where) conditions = where.map do |field, value| if value.is_a?(Hash) # Handle operators like $gt, $lt, etc. value.map do |op, val| { op => ["$$item.#{field}", val] } end else # Simple equality { "$eq" => ["$$item.#{field}", value] } end end.flatten if conditions.length == 1 conditions.first else { "$and" => conditions } end end |
#build_include_lookup_stages(includes) ⇒ Array<Hash>
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Build $lookup stages for included pointer fields in direct MongoDB queries. This enables eager loading of related objects when using results_direct.
2497 2498 2499 2500 2501 2502 2503 2504 2505 2506 2507 2508 2509 2510 2511 2512 2513 2514 2515 2516 2517 2518 2519 2520 2521 2522 2523 2524 2525 2526 2527 2528 2529 2530 2531 2532 2533 2534 2535 2536 2537 2538 2539 2540 2541 2542 2543 2544 2545 2546 2547 2548 2549 2550 2551 2552 2553 |
# File 'lib/parse/query.rb', line 2497 def build_include_lookup_stages(includes) return [] if includes.nil? || includes.empty? stages = [] includes.each do |field| # Handle nested includes (e.g., 'artist.label') - only process first level field_str = field.to_s base_field = field_str.split(".").first.to_sym # Get target class from model references target_class = get_pointer_target_class(base_field) next unless target_class # MongoDB pointer field name mongo_pointer_field = "_p_#{base_field}" lookup_result_field = "_included_#{base_field}" lookup_id_field = "_include_id_#{base_field}" # Stage 1: Extract objectId from pointer string using $split # Parse pointers are stored as "ClassName$objectId" stages << { "$addFields" => { lookup_id_field => { "$arrayElemAt" => [ { "$split" => ["$#{mongo_pointer_field}", { "$literal" => "$" }] }, 1, ], }, }, } # Stage 2: $lookup to join with target collection stages << { "$lookup" => { "from" => target_class, "localField" => lookup_id_field, "foreignField" => "_id", "as" => lookup_result_field, }, } # Stage 3: Unwind the array (since $lookup returns array, but we want single object) stages << { "$unwind" => { "path" => "$#{lookup_result_field}", "preserveNullAndEmptyArrays" => true, }, } # Stage 4: Clean up temporary lookup ID field stages << { "$unset" => lookup_id_field, } end stages end |
#clause(clause_name = :where) ⇒ Object
returns the query clause for the particular clause
586 587 588 589 |
# File 'lib/parse/query.rb', line 586 def clause(clause_name = :where) return unless [:keys, :where, :order, :includes, :limit, :skip].include?(clause_name) instance_variable_get "@#{clause_name}".to_sym end |
#clear(item = :results) ⇒ self
Clear a specific clause of this query. This can be one of: :where, :order, :includes, :skip, :limit, :count, :keys or :results.
434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 |
# File 'lib/parse/query.rb', line 434 def clear(item = :results) case item when :where # an array of Parse::Constraint subclasses @where = [] when :order # an array of Parse::Order objects @order = [] when :includes @includes = [] when :skip @skip = 0 when :limit @limit = nil when :count @count = 0 when :keys @keys = [] end @results = nil self # chaining end |
#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
5206 5207 5208 5209 5210 5211 5212 5213 5214 5215 5216 5217 5218 5219 5220 5221 5222 5223 5224 5225 5226 5227 5228 5229 5230 |
# File 'lib/parse/query.rb', line 5206 def clone cloned_query = Parse::Query.new(self.instance_variable_get(:@table)) # Note: :client is intentionally excluded - it contains non-serializable objects # (Redis connections, Faraday connections) and should be obtained lazily [:count, :where, :order, :keys, :includes, :limit, :skip, :cache, :use_master_key].each do |param| if instance_variable_defined?(:"@#{param}") value = instance_variable_get(:"@#{param}") if value.is_a?(Array) || value.is_a?(Hash) # Use Marshal for deep copy of complex constraint objects begin cloned_value = Marshal.load(Marshal.dump(value)) rescue => e # Fallback to shallow copy if Marshal fails puts "[Parse::Query.clone] Marshal failed for #{param}: #{e.}, 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.
3924 3925 3926 3927 3928 3929 3930 3931 3932 3933 3934 3935 3936 3937 3938 3939 3940 3941 3942 3943 3944 3945 3946 3947 3948 3949 3950 3951 3952 3953 3954 3955 3956 3957 |
# File 'lib/parse/query.rb', line 3924 def compile(encode: true, includeClassName: false) # Validate includes vs keys before compiling validate_includes_vs_keys # When a `keys` allowlist is set alongside `include`, the parent pointer # field must also be in `keys` or Parse Server strips it before expanding # the include. Auto-add the top-level segment of each include so partial # fetches don't silently drop included pointers. merge_includes_into_keys! run_callbacks :prepare do q = {} #query q[:limit] = @limit if @limit.is_a?(Numeric) && @limit > 0 q[:skip] = @skip if @skip > 0 q[:include] = @includes.join(",") unless @includes.empty? q[:keys] = @keys.join(",") unless @keys.empty? q[:order] = @order.join(",") unless @order.empty? unless @where.empty? q[:where] = Parse::Query.compile_where(@where) q[:where] = q[:where].to_json if encode end if @count && @count > 0 # if count is requested q[:limit] = 0 q[:count] = 1 end if includeClassName q[:className] = @table end q end end |
#compile_where ⇒ Hash
Returns a hash representing just the where clause of this
query, with SDK-internal routing markers stripped.
3961 3962 3963 |
# File 'lib/parse/query.rb', line 3961 def compile_where self.class.compile_where(@where || []) end |
#conditions(expressions = {}) ⇒ self Also known as: query, append
Add a set of query expressions and constraints.
507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 |
# File 'lib/parse/query.rb', line 507 def conditions(expressions = {}) expressions.each do |expression, value| # Normalize to symbol for comparison (handles both string and symbol keys) expr_sym = expression.respond_to?(:to_sym) ? expression.to_sym : expression if expr_sym == :order order value elsif expr_sym == :keys keys value elsif expr_sym == :key keys [value] elsif expr_sym == :skip skip value elsif expr_sym == :limit limit value elsif expr_sym == :include || expr_sym == :includes includes(value) elsif expr_sym == :cache self.cache = value elsif expr_sym == :use_master_key self.use_master_key = value elsif expr_sym == :session # you can pass a session token or a Parse::Session self.session_token = value elsif expr_sym == :read_preference self.read_preference = value # ACL convenience query options elsif expr_sym == :readable_by readable_by(value) elsif expr_sym == :writable_by writable_by(value) elsif expr_sym == :readable_by_role readable_by_role(value) elsif expr_sym == :writable_by_role writable_by_role(value) elsif expr_sym == :publicly_readable publicly_readable if value elsif expr_sym == :publicly_writable publicly_writable if value elsif expr_sym == :privately_readable || expr_sym == :master_key_read_only privately_readable if value elsif expr_sym == :privately_writable || expr_sym == :master_key_write_only privately_writable if value elsif expr_sym == :private_acl || expr_sym == :master_key_only private_acl if value elsif expr_sym == :not_publicly_readable not_publicly_readable if value elsif expr_sym == :not_publicly_writable not_publicly_writable if value else add_constraint(expression, value) end end # each self #chaining end |
#constraints(raw = false) ⇒ Array<Parse::Constraint>, Hash
927 928 929 |
# File 'lib/parse/query.rb', line 927 def constraints(raw = false) raw ? where_constraints : @where end |
#convert_addfields_for_direct_mongodb(spec) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Convert a $addFields / $set stage for direct MongoDB. Same shape
as $project: { aliasName => <expression> }. Output aliases pass
through verbatim; each value is walked as an aggregation
expression so storage-column references inside reach the correct
column via the schema-aware #convert_field_for_direct_mongodb.
2899 2900 2901 2902 2903 2904 2905 2906 2907 |
# File 'lib/parse/query.rb', line 2899 def convert_addfields_for_direct_mongodb(spec) return spec unless spec.is_a?(Hash) result = {} spec.each do |field, value| result[field] = rewrite_expression_for_direct_mongodb(value) end result end |
#convert_constraints_for_direct_mongodb(constraints) ⇒ Hash
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Convert constraints for direct MongoDB execution.
2591 2592 2593 2594 2595 2596 2597 2598 2599 2600 2601 2602 2603 2604 2605 2606 2607 2608 2609 2610 2611 2612 2613 2614 2615 2616 2617 2618 2619 2620 2621 2622 2623 2624 2625 2626 2627 2628 2629 2630 2631 2632 2633 2634 2635 2636 2637 |
# File 'lib/parse/query.rb', line 2591 def convert_constraints_for_direct_mongodb(constraints) return constraints unless constraints.is_a?(Hash) # $relatedTo resolves a Parse Relation, which is stored in the # `_Join:<key>:<ParentClass>` collection — a join the SDK does NOT # translate on the mongo-direct path. Passed through verbatim it reaches # MongoDB as an unknown `$match` operator and fails with an opaque error; # and any future attempt to rewrite it into a `$lookup` would have to # re-implement the `_rperm` / protectedFields enforcement that the rest of # this path applies post-fetch. Parse Server's own `$relatedTo` was found # to bypass exactly that enforcement (GHSA-wmwx-jr2p-4j4r), so fail closed # here with a clear message rather than risk a silent leak: this query # must run via REST (the default), where Parse Server resolves the # relation under its own ACL / CLP enforcement. if constraints.key?("$relatedTo") || constraints.key?(:"$relatedTo") raise ArgumentError, "[Parse::Query] $relatedTo cannot run on the mongo-direct path; a " \ "Parse Relation is resolved server-side via its join collection. Run " \ "this query via REST (omit `mongo_direct:` / `.results_direct` and any " \ "direct-only constraint), or express the membership as an `$inQuery` " \ "against the relation's join collection." end result = {} constraints.each do |field, value| field_str = field.to_s # Skip special operators if field_str.start_with?("$") # Recursively convert nested constraints in $and, $or, $nor if value.is_a?(Array) && %w[$and $or $nor].include?(field_str) result[field_str] = value.map { |v| convert_constraints_for_direct_mongodb(v) } else result[field_str] = value end next end # Convert field name for MongoDB mongo_field = convert_field_for_direct_mongodb(field_str) # Convert value result[mongo_field] = convert_value_for_direct_mongodb(field_str, value) end result end |
#convert_field_for_direct_mongodb(field) ⇒ String
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Convert a field name for direct MongoDB access.
2643 2644 2645 2646 2647 2648 2649 2650 2651 2652 2653 2654 2655 2656 2657 2658 2659 2660 2661 2662 2663 2664 2665 2666 2667 2668 2669 2670 2671 2672 2673 2674 2675 2676 2677 2678 2679 2680 2681 2682 2683 2684 2685 2686 2687 2688 2689 2690 2691 2692 2693 2694 2695 2696 2697 2698 2699 2700 2701 2702 2703 2704 2705 2706 2707 2708 2709 2710 2711 2712 2713 2714 2715 2716 2717 2718 2719 2720 2721 2722 2723 2724 2725 2726 2727 |
# File 'lib/parse/query.rb', line 2643 def convert_field_for_direct_mongodb(field) field_str = field.to_s # Any field name starting with underscore is non-user-facing and is # passed through verbatim. Parse user-facing properties never start # with `_` (the SDK columnizes snake_case to camelCase before save, # and Parse Server reserves the leading-underscore namespace), so a # field that does is one of: # - a MongoDB/Parse Server internal column (`_id`, `_created_at`, # `_acl`, `_rperm`, `_wperm`, `_hashed_password`, # `_session_token`, `_email_verify_token`, ...) # - a Parse-on-Mongo pointer storage column (`_p_<field>`) # - an SDK-built pipeline-temp alias such as the # `_lookup_<field>_result` / `_lookup_<field>_id` aliases that # `extract_subquery_to_lookup_stages` introduces when an # `$inQuery` constraint compiles to a `$lookup` stage # Columnizing any of these would corrupt the reference: the # previous behavior of routing `_lookup_project_result` through # `format_field` produced `lookupProjectResult` (leading underscore # stripped, snake_case to camelCase), and the post-lookup # `$match` then asked MongoDB for a column that didn't exist, so # every document silently satisfied the constraint. return field_str if field_str.start_with?("_") # Apply field formatting for regular fields formatted = Query.format_field(field) case formatted when "objectId" "_id" when "createdAt" "_created_at" when "updatedAt" "_updated_at" else # Schema-aware passthrough: only rewrite names that correspond # to a declared Parse property (or the universal built-ins # handled above). Anything else is treated as a pipeline-local # alias — `$group` accumulator name, `$project` computed field, # `$addFields` output — and the literal text passes through so # the reference matches the output key the upstream stage # produced. # # Concretely: `$status` on a class that declares `status` # remains `status` (`format_field` is a no-op for already- # camelCase names); `$author` on a class that declares a # pointer `author` becomes `$_p_author`; `$contributor_set` # (an alias the caller introduced in `$group`) stays # `$contributor_set` because no such property exists in the # schema. Callers reading the result row by `row[alias_name]` # see exactly the spelling they wrote into the pipeline. # # @note Two documented limitations of the schema-aware rule: # # 1. **Alias shadowing.** An alias whose name shadows a # declared Parse property (`$group { author: ... }` where # `author` is a pointer) is treated as the property — # downstream `$author` references resolve to `$_p_author`, # the storage column, not the alias. Avoid alias names that # collide with declared property names. The same naming # constraint MongoDB aggregation pipelines have generally; # not unique to parse-stack. # # 2. **Undeclared server columns.** Conversely, a `$field` # reference whose name corresponds to a column that exists # on the server but is NOT declared as a property on the # Ruby model passes through verbatim. The schema we consult # is the SDK-side property registry; we do not introspect # the live server schema on every translation. If you need # references in mongo-direct pipelines to translate # snake_case → camelCase or take a `_p_*` prefix, declare # the corresponding property on the Ruby model. Workaround # without declaring: write the storage-column name directly # (`$_p_author`, `$companyName`), which short-circuits the # walker via the leading-underscore / already-formatted # paths. return field_str unless field_is_known_to_schema?(formatted) if field_is_pointer?(formatted) "_p_#{formatted}" else formatted end end end |
#convert_group_for_direct_mongodb(group) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Convert $group stage for direct MongoDB. Output-alias keys
(_id, accumulator names like contributor_set) pass through
verbatim so the result row uses whatever spelling the caller
wrote. Each value — the _id group-key expression and every
accumulator expression — is walked as an aggregation expression
so $field references inside reach the correct storage column
(_p_* for pointers, _id/_created_at/_updated_at for
built-ins, untouched for unknown names i.e. pipeline-local
aliases) via the schema-aware
#convert_field_for_direct_mongodb.
2883 2884 2885 2886 2887 2888 2889 2890 2891 |
# File 'lib/parse/query.rb', line 2883 def convert_group_for_direct_mongodb(group) return group unless group.is_a?(Hash) result = {} group.each do |field, value| result[field] = rewrite_expression_for_direct_mongodb(value) end result end |
#convert_match_for_direct_mongodb(match) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Convert a $match stage for direct MongoDB. Rewrites top-level field-name keys via #convert_constraints_for_direct_mongodb and additionally walks the value of a top-level $expr as an aggregation expression so nested $fieldName references are rewritten.
2827 2828 2829 2830 2831 2832 2833 2834 2835 2836 2837 2838 2839 2840 |
# File 'lib/parse/query.rb', line 2827 def convert_match_for_direct_mongodb(match) converted = convert_constraints_for_direct_mongodb(match) return converted unless converted.is_a?(Hash) # The constraint converter passes $expr through unchanged. Rewrite # its value here so e.g. {$expr: {$eq: ["$author", "$approver"]}} # becomes {$expr: {$eq: ["$_p_author", "$_p_approver"]}}. expr_key = converted.key?("$expr") ? "$expr" : (converted.key?(:"$expr") ? :"$expr" : nil) return converted unless expr_key result = converted.dup result[expr_key] = rewrite_expression_for_direct_mongodb(converted[expr_key]) result end |
#convert_projection_for_direct_mongodb(projection) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Convert projection fields for direct MongoDB. Output-key aliases
pass through verbatim — what the caller writes is what the result
row will be keyed by. Values that are aggregation expressions
(e.g. { "$cond": [...] }) are walked recursively so nested
$fieldName references reach the correct storage column via the
schema-aware rewriter in #convert_field_for_direct_mongodb.
2849 2850 2851 2852 2853 2854 2855 2856 2857 |
# File 'lib/parse/query.rb', line 2849 def convert_projection_for_direct_mongodb(projection) return projection unless projection.is_a?(Hash) result = {} projection.each do |field, value| result[field] = rewrite_expression_for_direct_mongodb(value) end result end |
#convert_replace_root_for_direct_mongodb(spec) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Convert a $replaceRoot stage for direct MongoDB. Argument shape is
{ newRoot: <expression> }; only the newRoot value is an
expression. (Use #rewrite_expression_for_direct_mongodb directly
for $replaceWith, whose argument is the expression itself.)
2914 2915 2916 2917 2918 2919 2920 2921 2922 2923 |
# File 'lib/parse/query.rb', line 2914 def convert_replace_root_for_direct_mongodb(spec) return rewrite_expression_for_direct_mongodb(spec) unless spec.is_a?(Hash) new_root_key = spec.key?("newRoot") ? "newRoot" : (spec.key?(:newRoot) ? :newRoot : nil) return rewrite_expression_for_direct_mongodb(spec) unless new_root_key result = spec.dup result[new_root_key] = rewrite_expression_for_direct_mongodb(spec[new_root_key]) result end |
#convert_sort_for_direct_mongodb(sort) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Convert sort specification for direct MongoDB.
2861 2862 2863 2864 2865 2866 2867 2868 2869 2870 |
# File 'lib/parse/query.rb', line 2861 def convert_sort_for_direct_mongodb(sort) return sort unless sort.is_a?(Hash) result = {} sort.each do |field, direction| mongo_field = convert_field_for_direct_mongodb(field) result[mongo_field] = direction end result end |
#convert_stage_for_direct_mongodb(stage) ⇒ Hash
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Convert an aggregation stage for direct MongoDB execution.
Projection-shape stages ($project, $addFields, $set, $replaceRoot, $replaceWith) and accumulator/grouping stages ($group) carry aggregation expressions that can reference fields via $fieldName strings. These references must be rewritten to the direct-MongoDB column form (camelCase, p* for pointers, _id/_created_at/_updated_at for built-ins). The rewrite walks recursively into $cond / $eq / $switch / $expr argument arrays so a nested reference is not missed. See #rewrite_expression_for_direct_mongodb.
$match is special: its top-level keys are field-name constraints (rewritten via the constraint converter), but the value of a top-level $expr is an aggregation expression that must also be walked.
2794 2795 2796 2797 2798 2799 2800 2801 2802 2803 2804 2805 2806 2807 2808 2809 2810 2811 2812 2813 2814 2815 2816 2817 2818 2819 2820 |
# File 'lib/parse/query.rb', line 2794 def convert_stage_for_direct_mongodb(stage) return stage unless stage.is_a?(Hash) result = {} stage.each do |operator, value| case operator.to_s when "$match" result[operator] = convert_match_for_direct_mongodb(value) when "$project" result[operator] = convert_projection_for_direct_mongodb(value) when "$sort" result[operator] = convert_sort_for_direct_mongodb(value) when "$group" result[operator] = convert_group_for_direct_mongodb(value) when "$addFields", "$set" result[operator] = convert_addfields_for_direct_mongodb(value) when "$replaceRoot" result[operator] = convert_replace_root_for_direct_mongodb(value) when "$replaceWith" # $replaceWith's argument is the new-root expression directly. result[operator] = rewrite_expression_for_direct_mongodb(value) else result[operator] = value end end result end |
#convert_value_for_direct_mongodb(field, value) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Convert a value for direct MongoDB execution.
2734 2735 2736 2737 2738 2739 2740 2741 2742 2743 2744 2745 2746 2747 2748 2749 2750 2751 2752 2753 2754 2755 2756 2757 2758 2759 2760 2761 2762 2763 2764 2765 2766 2767 2768 2769 2770 2771 2772 2773 2774 2775 |
# File 'lib/parse/query.rb', line 2734 def convert_value_for_direct_mongodb(field, value) case value when Hash # Handle both string and symbol keys for __type checks type_value = value["__type"] || value[:__type] if type_value == "Pointer" # Convert Parse pointer to MongoDB pointer string format class_name = value["className"] || value[:className] object_id = value["objectId"] || value[:objectId] "#{class_name}$#{object_id}" elsif type_value == "Date" # Convert Parse Date format to Time object for BSON Date iso_value = value["iso"] || value[:iso] Time.parse(iso_value).utc else # Recursively convert nested hash (for operators like $gt, $in, etc.) # Convert symbol keys to strings for MongoDB converted = {} value.each do |k, v| key_str = k.to_s converted[key_str] = convert_value_for_direct_mongodb(field, v) end converted end when Parse::Pointer "#{value.parse_class}$#{value.id}" when Parse::Date # Parse::Date extends DateTime - convert to Time for BSON Date value.to_time.utc when Time value.utc when DateTime value.to_time.utc when Date value.to_time.utc when Array value.map { |v| convert_value_for_direct_mongodb(field, v) } else value end end |
#count(mongo_direct: false) ⇒ Integer
Perform a count query.
1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 |
# File 'lib/parse/query.rb', line 1154 def count(mongo_direct: false) # Use direct MongoDB query if requested return count_direct if mongo_direct # Auto-route to mongo-direct when the compiled where contains a # direct-only constraint. Same gate as #results. if requires_mongo_direct? assert_mongo_direct_routable! return count_direct(**mongo_direct_auth_kwargs) end # Check if this query requires aggregation pipeline processing if requires_aggregation_pipeline? # Build aggregation pipeline with $count stage pipeline, has_lookup_stages = build_aggregation_pipeline pipeline << { "$count" => "count" } # Auto-detect if MongoDB direct is needed use_mongo_direct = false if has_lookup_stages && defined?(Parse::MongoDB) && Parse::MongoDB.enabled? use_mongo_direct = true end # Execute aggregation aggregation = Aggregation.new(self, pipeline, verbose: @verbose_aggregate, mongo_direct: use_mongo_direct) response = aggregation.execute! # Extract count from aggregation result if use_mongo_direct # MongoDB direct returns raw array return 0 if response.nil? || response.empty? response.first["count"] || 0 else return 0 if response.error? || !response.result.is_a?(Array) || response.result.empty? response.result.first["count"] || 0 end else # Use standard count endpoint for non-aggregation queries old_value = @count @count = 1 res = client.find_objects(@table, compile.as_json, **_opts).count @count = old_value res end end |
#count_direct(session_token: nil, master: nil, acl_user: nil, acl_role: nil) ⇒ Integer
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.
2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034 2035 2036 2037 2038 2039 2040 2041 2042 2043 2044 2045 2046 2047 2048 2049 2050 |
# File 'lib/parse/query.rb', line 2007 def count_direct(session_token: nil, master: nil, acl_user: nil, acl_role: nil) require_relative "mongodb" Parse::MongoDB.require_gem! unless Parse::MongoDB.available? raise Parse::MongoDB::NotEnabled, "Direct MongoDB queries are not enabled. " \ "Call Parse::MongoDB.configure(uri: 'mongodb://...', enabled: true) first." end # Build the aggregation pipeline for direct MongoDB execution pipeline = build_direct_mongodb_pipeline # Remove limit and skip for count (we want total count) pipeline = pipeline.reject { |stage| stage.key?("$limit") || stage.key?("$skip") } # Add count stage pipeline << { "$count" => "count" } # When no explicit auth kwargs are provided, derive them from the # query's own auth state — same fallback as results_direct. if session_token.nil? && master.nil? && acl_user.nil? && acl_role.nil? auth = mongo_direct_auth_kwargs session_token = auth[:session_token] master = auth[:master] acl_user = auth[:acl_user] acl_role = auth[:acl_role] end # SDK-built pipeline only — see results_direct for rationale. # ACL simulation runs inside Parse::MongoDB.aggregate when # session_token: or master: is supplied. raw_results = Parse::MongoDB.aggregate(@table, pipeline, allow_internal_fields: true, session_token: session_token, master: master, acl_user: acl_user, acl_role: acl_role, read_preference: @read_preference) # Extract count from result return 0 if raw_results.empty? raw_results.first["count"] || 0 end |
#count_distinct(field) ⇒ Integer
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.
1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 |
# File 'lib/parse/query.rb', line 1212 def count_distinct(field) if field.nil? || !field.respond_to?(:to_s) raise ArgumentError, "Invalid field name passed to `count_distinct`." end # Format field name according to Parse conventions # Handle special MongoDB field mappings for aggregation formatted_field = case field.to_s when "created_at", "createdAt" "_created_at" when "updated_at", "updatedAt" "_updated_at" else Query.format_field(field) end # Build the aggregation pipeline pipeline = [ { "$group" => { "_id" => "$#{formatted_field}" } }, { "$count" => "distinctCount" }, ] # Use the Aggregation class to execute # The aggregate method will automatically handle where conditions aggregation = aggregate(pipeline, verbose: @verbose_aggregate) raw_results = aggregation.raw # Extract the count from the response if raw_results.is_a?(Array) && raw_results.first raw_results.first["distinctCount"] || 0 else 0 end end |
#cursor(limit: 100, order: nil) ⇒ Parse::Cursor
Create a cursor-based paginator for efficiently traversing large datasets.
Cursor-based pagination is more efficient than skip/offset pagination for large datasets because it uses the last seen objectId to fetch the next page, rather than skipping over records.
2992 2993 2994 |
# File 'lib/parse/query.rb', line 2992 def cursor(limit: 100, order: nil) Parse::Cursor.new(self, limit: limit, order: order) end |
#decode(list) ⇒ Array<Parse::Object>
Builds objects based on the set of Parse JSON hashes in an array.
3789 3790 3791 3792 3793 3794 3795 3796 3797 3798 |
# File 'lib/parse/query.rb', line 3789 def decode(list) # Pass fetched keys for partial fetch tracking (only if keys were specified) fetch_keys = @keys.present? && @keys.any? ? @keys : nil # Parse keys (not includes) to build nested fetched keys map # Keys like ["project.name", "project.status"] define which subfields to fetch on nested objects nested_keys = Parse::Query.parse_keys_to_nested_keys(@keys) if @keys.present? list.map { |m| Parse::Object.build(m, @table, fetched_keys: fetch_keys, nested_fetched_keys: nested_keys) }.compact end |
#deduplicate_consecutive_match_stages(pipeline) ⇒ Array<Hash>
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Merge consecutive $match stages in an aggregation pipeline. This optimization combines redundant stages that can occur when building pipelines from multiple constraint sources. Identical stages are deduplicated, and non-identical consecutive $match stages are merged using $and.
3083 3084 3085 3086 3087 3088 3089 3090 3091 3092 3093 3094 3095 3096 3097 3098 3099 3100 3101 3102 3103 3104 3105 3106 3107 3108 |
# File 'lib/parse/query.rb', line 3083 def deduplicate_consecutive_match_stages(pipeline) return pipeline if pipeline.empty? result = [] pipeline.each do |stage| if stage.is_a?(Hash) && stage.key?("$match") && result.last.is_a?(Hash) && result.last.key?("$match") prev_match = result.last["$match"] curr_match = stage["$match"] # Skip if identical next if prev_match == curr_match # Merge the two $match stages using $and # Handle cases where either side might already have $and prev_conditions = prev_match.key?("$and") ? prev_match["$and"] : [prev_match] curr_conditions = curr_match.key?("$and") ? curr_match["$and"] : [curr_match] # Replace the previous $match with the merged version result[-1] = { "$match" => { "$and" => prev_conditions + curr_conditions } } else result << stage end end result end |
#distinct(field, return_pointers: false, mongo_direct: false, order: nil) ⇒ Object
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.
1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 |
# File 'lib/parse/query.rb', line 1032 def distinct(field, return_pointers: false, mongo_direct: false, order: nil) # Explicit opt-in to direct MongoDB if mongo_direct return distinct_direct(field, return_pointers: return_pointers, order: order, **mongo_direct_auth_kwargs) end # Auto-route to mongo-direct when the compiled where contains a # direct-only constraint. Same gate as #count / #results. if requires_mongo_direct? assert_mongo_direct_routable! return distinct_direct(field, return_pointers: return_pointers, order: order, **mongo_direct_auth_kwargs) end # Auto-route scoped queries (session_token / acl_user / acl_role) to # mongo-direct: Parse Server's REST `/aggregate` endpoint is # master-key-only and enforces neither ACL nor CLP, so a scoped # `.distinct` call against REST would silently return unscoped # values. The mongo-direct path runs ACLScope + CLPScope before # `$group`, so distinct values reflect only ACL-readable rows. if distinct_query_is_scoped? && defined?(Parse::MongoDB) && Parse::MongoDB.enabled? return distinct_direct(field, return_pointers: return_pointers, order: order, **mongo_direct_auth_kwargs) end if field.nil? || !field.respond_to?(:to_s) || field.is_a?(Hash) || field.is_a?(Array) raise ArgumentError, "Invalid field name passed to `distinct`." end sort_dir = distinct_sort_direction(order) # Format field for aggregation formatted_field = format_aggregation_field(field) # Build the aggregation pipeline for distinct values pipeline = [{ "$group" => { "_id" => "$#{formatted_field}" } }] pipeline << { "$sort" => { "_id" => sort_dir } } if sort_dir pipeline << { "$project" => { "_id" => 0, "value" => "$_id" } } # Add match stage if there are where conditions compiled_where = compile_where if compiled_where.present? # Convert field names for aggregation context and handle dates aggregation_where = convert_constraints_for_aggregation(compiled_where) stringified_where = convert_dates_for_aggregation(aggregation_where) pipeline.unshift({ "$match" => stringified_where }) end # Use the Aggregation class to execute aggregation = aggregate(pipeline, verbose: @verbose_aggregate) raw_results = aggregation.raw # Extract values from the results values = raw_results.map { |item| item["value"] }.compact # Use schema-based approach to handle pointer field results parse_class = Parse::Model.const_get(@table) rescue nil is_pointer = parse_class && is_pointer_field?(parse_class, field, formatted_field) if is_pointer && values.any? # Convert all values using schema information converted_values = values.map do |value| convert_pointer_value_with_schema(value, field, return_pointers: return_pointers) end converted_values elsif return_pointers # Explicit conversion requested - try to convert using schema or fallback to string detection if values.any? && values.first.is_a?(String) && values.first.include?("$") to_pointers(values, field) else values.map { |value| convert_pointer_value_with_schema(value, field, return_pointers: true) } end else # Fallback to original string detection for backward compatibility if values.any? && values.first.is_a?(String) && values.first.include?("$") && values.first.match(/^[A-Za-z]\w*\$\w+$/) first_class_name = values.first.split("$", 2)[0] if values.all? { |v| v.is_a?(String) && v.start_with?("#{first_class_name}$") } values.map { |value| value.split("$", 2)[1] } else values end else values end end end |
#distinct_direct(field, return_pointers: false, order: nil, session_token: nil, master: nil, acl_user: nil, acl_role: nil) ⇒ Array
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.
2070 2071 2072 2073 2074 2075 2076 2077 2078 2079 2080 2081 2082 2083 2084 2085 2086 2087 2088 2089 2090 2091 2092 2093 2094 2095 2096 2097 2098 2099 2100 2101 2102 2103 2104 2105 2106 2107 2108 2109 2110 2111 2112 2113 2114 2115 2116 2117 2118 2119 2120 2121 2122 2123 2124 2125 2126 2127 2128 2129 2130 2131 2132 2133 2134 2135 2136 2137 2138 2139 2140 2141 2142 2143 2144 2145 2146 |
# File 'lib/parse/query.rb', line 2070 def distinct_direct(field, return_pointers: false, order: nil, session_token: nil, master: nil, acl_user: nil, acl_role: nil) require_relative "mongodb" Parse::MongoDB.require_gem! unless Parse::MongoDB.available? raise Parse::MongoDB::NotEnabled, "Direct MongoDB queries are not enabled. " \ "Call Parse::MongoDB.configure(uri: 'mongodb://...', enabled: true) first." end if field.nil? || !field.respond_to?(:to_s) || field.is_a?(Hash) || field.is_a?(Array) raise ArgumentError, "Invalid field name passed to `distinct_direct`." end sort_dir = distinct_sort_direction(order) # Convert field name for direct MongoDB access mongo_field = convert_field_for_direct_mongodb(Query.format_field(field)) # Build the base pipeline with match constraints pipeline = [] # Add match stage from query constraints. `compile_where` already # strips `__`-prefixed routing markers, so the result is safe to # forward to MongoDB. compiled_where = compile_where if compiled_where.present? mongo_constraints = convert_constraints_for_direct_mongodb(compiled_where) pipeline << { "$match" => mongo_constraints } if mongo_constraints.any? end # Add group, optional sort, and project stages for distinct pipeline << { "$group" => { "_id" => "$#{mongo_field}" } } pipeline << { "$sort" => { "_id" => sort_dir } } if sort_dir pipeline << { "$project" => { "_id" => 0, "value" => "$_id" } } # SDK-built pipeline only — see results_direct for rationale. # Forward auth kwargs so Parse::MongoDB.aggregate runs the # three-layer ACL + CLP + protectedFields simulation for scoped # agents. Without this, distinct silently returns the unscoped # universe (CLP-1 enforcement asymmetry vs. #count / #results). # When no explicit auth kwargs are provided, derive from the # query's own auth state — same fallback as results_direct. if session_token.nil? && master.nil? && acl_user.nil? && acl_role.nil? auth = mongo_direct_auth_kwargs session_token = auth[:session_token] master = auth[:master] acl_user = auth[:acl_user] acl_role = auth[:acl_role] end raw_results = Parse::MongoDB.aggregate(@table, pipeline, allow_internal_fields: true, read_preference: @read_preference, session_token: session_token, master: master, acl_user: acl_user, acl_role: acl_role) # Extract values from results values = raw_results.map { |doc| doc["value"] }.compact # Handle pointer conversion if needed if return_pointers || field_is_pointer?(Query.format_field(field)) values = values.map do |value| if value.is_a?(String) && value.include?("$") # MongoDB pointer format: "ClassName$objectId" class_name, object_id = value.split("$", 2) Parse::Pointer.new(class_name, object_id) else value end end end values end |
#distinct_direct_pointers(field, order: nil, session_token: nil, master: nil, acl_user: nil, acl_role: nil) ⇒ Array
Convenience method for distinct_direct that always returns Parse::Pointer objects for pointer fields.
2153 2154 2155 2156 2157 2158 |
# File 'lib/parse/query.rb', line 2153 def distinct_direct_pointers(field, order: nil, session_token: nil, master: nil, acl_user: nil, acl_role: nil) distinct_direct(field, return_pointers: true, order: order, session_token: session_token, master: master, acl_user: acl_user, acl_role: acl_role) end |
#distinct_objects(field, return_pointers: false) ⇒ Array
Enhanced distinct method that automatically populates Parse pointer objects at the server level. Uses aggregation pipeline to efficiently populate objects instead of post-processing.
4298 4299 4300 4301 4302 4303 4304 4305 |
# File 'lib/parse/query.rb', line 4298 def distinct_objects(field, return_pointers: false) if field.nil? || !field.respond_to?(:to_s) raise ArgumentError, "Invalid field name passed to `distinct_objects`." end # Use aggregation pipeline to get distinct values with populated objects execute_distinct_with_population(field, return_pointers: return_pointers) end |
#distinct_pointers(field, order: nil) ⇒ Array
Convenience method for distinct queries that always return Parse::Pointer objects for pointer fields. This is equivalent to calling distinct(field, return_pointers: true).
1125 1126 1127 |
# File 'lib/parse/query.rb', line 1125 def distinct_pointers(field, order: nil) distinct(field, return_pointers: true, order: order) end |
#distinct_query_is_scoped? ⇒ Boolean
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Whether this query carries a non-master-key auth scope. Used by
#distinct (and group_by aggregations) to decide whether to
auto-promote the REST aggregate path to mongo-direct so the SDK's
ACLScope / CLPScope enforcement actually runs.
1614 1615 1616 1617 1618 1619 |
# File 'lib/parse/query.rb', line 1614 def distinct_query_is_scoped? return true if @session_token.is_a?(String) && !@session_token.empty? return true if @acl_user return true if @acl_role false end |
#each { ... } ⇒ Array
1250 1251 1252 1253 |
# File 'lib/parse/query.rb', line 1250 def each(&block) return results.enum_for(:each) unless block_given? # Sparkling magic! results.each(&block) end |
#execute_aggregation_pipeline ⇒ Aggregation
Execute an aggregation pipeline for queries with pipeline constraints
3485 3486 3487 3488 3489 3490 3491 3492 3493 3494 3495 3496 3497 3498 3499 3500 3501 3502 3503 3504 3505 3506 3507 3508 3509 |
# File 'lib/parse/query.rb', line 3485 def execute_aggregation_pipeline pipeline, has_lookup_stages = build_aggregation_pipeline # Determine if MongoDB direct should be used: # 1. Explicit opt-in via @acl_query_mongo_direct = true # 2. Auto-detect when lookup stages use $split with $literal (to parse pointer format), # Parse Server's REST API can't handle it correctly # 3. Auto-detect when querying internal fields like _rperm or _wperm (ACL fields), # Parse Server blocks these for security - must use MongoDB direct use_mongo_direct = false # Check for explicit mongo_direct preference first if defined?(@acl_query_mongo_direct) && !@acl_query_mongo_direct.nil? use_mongo_direct = @acl_query_mongo_direct elsif defined?(Parse::MongoDB) && Parse::MongoDB.enabled? # Auto-detect based on pipeline contents if has_lookup_stages || pipeline_uses_internal_fields?(pipeline) use_mongo_direct = true end end # Create Aggregation directly to avoid double-applying constraints # The aggregate() method would redundantly add where constraints again Aggregation.new(self, pipeline, verbose: @verbose_aggregate, mongo_direct: use_mongo_direct) end |
#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.
3065 3066 3067 3068 3069 3070 3071 3072 3073 3074 |
# File 'lib/parse/query.rb', line 3065 def explain compiled_query = compile compiled_query[:explain] = true response = client.find_objects(@table, compiled_query.as_json, **_opts) if response.error? puts "[ParseQuery:Explain] #{response.error}" return {} end response.result end |
#extract_subquery_to_lookup_stages(constraints) ⇒ Hash
Extract $inQuery and $notInQuery constraints and build $lookup stages for them. This converts Parse subquery constraints into MongoDB $lookup stages that join with the related collection and filter based on the subquery conditions. Uses raw MongoDB field names (_p_field) and returns results via .raw aggregation.
3639 3640 3641 3642 3643 3644 3645 3646 3647 3648 3649 3650 3651 3652 3653 3654 3655 3656 3657 3658 3659 3660 3661 3662 3663 3664 3665 3666 3667 3668 3669 3670 3671 3672 3673 3674 3675 3676 3677 3678 3679 3680 3681 3682 3683 3684 3685 3686 3687 3688 3689 3690 3691 3692 3693 3694 3695 3696 3697 3698 3699 3700 3701 3702 3703 3704 3705 3706 3707 3708 3709 3710 3711 3712 3713 3714 3715 3716 3717 3718 3719 3720 3721 3722 3723 3724 3725 3726 3727 |
# File 'lib/parse/query.rb', line 3639 def extract_subquery_to_lookup_stages(constraints) return { constraints: constraints, lookup_stages: [], post_lookup_match: {} } unless constraints.is_a?(Hash) remaining_constraints = {} lookup_stages = [] post_lookup_match = {} constraints.each do |field, value| # Check for both string and symbol keys has_in_query = value.is_a?(Hash) && (value.key?("$inQuery") || value.key?(:"$inQuery")) has_not_in_query = value.is_a?(Hash) && (value.key?("$notInQuery") || value.key?(:"$notInQuery")) if has_in_query || has_not_in_query is_in_query = has_in_query # Get the subquery config using the correct key type in_query_key = value.key?("$inQuery") ? "$inQuery" : :"$inQuery" not_in_query_key = value.key?("$notInQuery") ? "$notInQuery" : :"$notInQuery" subquery_config = value[is_in_query ? in_query_key : not_in_query_key] # Handle both string and symbol keys in the subquery config class_name = subquery_config["className"] || subquery_config[:className] where_clause = subquery_config["where"] || subquery_config[:where] || {} # Format field name for the pointer formatted_field = Query.format_field(field) mongo_pointer_field = "_p_#{formatted_field}" lookup_result_field = "_lookup_#{formatted_field}_result" lookup_id_field = "_lookup_#{formatted_field}_id" # Stage 1: Extract objectId from the pointer field using $split # Parse Server stores pointers as _p_fieldName with format "ClassName$objectId" # Use $literal to escape the $ character in the delimiter lookup_stages << { "$addFields" => { lookup_id_field => { "$arrayElemAt" => [ { "$split" => ["$#{mongo_pointer_field}", { "$literal" => "$" }] }, 1, ], }, }, } # Stage 2: $lookup to join with the related collection # Build pipeline to match on _id and apply where conditions lookup_pipeline = [ { "$match" => { "$expr" => { "$eq" => ["$_id", "$$lookupId"] } } }, ] # Add where conditions to lookup pipeline if present if where_clause.any? converted_where = convert_dates_for_aggregation(where_clause) converted_where = convert_constraints_for_aggregation(converted_where) lookup_pipeline << { "$match" => converted_where } end lookup_stages << { "$lookup" => { "from" => class_name, "let" => { "lookupId" => "$#{lookup_id_field}" }, "pipeline" => lookup_pipeline, "as" => lookup_result_field, }, } # Match based on whether lookup returned results if is_in_query # $inQuery: keep documents where lookup found matches post_lookup_match[lookup_result_field] = { "$ne" => [] } else # $notInQuery: keep documents where lookup found no matches post_lookup_match[lookup_result_field] = { "$eq" => [] } end elsif value.is_a?(Hash) # Recursively handle nested constraints nested = extract_subquery_to_lookup_stages(value) if nested[:lookup_stages].any? lookup_stages.concat(nested[:lookup_stages]) post_lookup_match.merge!(nested[:post_lookup_match]) remaining_constraints[field] = nested[:constraints] else remaining_constraints[field] = value end else remaining_constraints[field] = value end end { constraints: remaining_constraints, lookup_stages: lookup_stages, post_lookup_match: post_lookup_match } end |
#fetch!(compiled_query) ⇒ Parse::Response Also known as: execute!
Performs the fetch request for the query.
1488 1489 1490 1491 1492 1493 1494 |
# File 'lib/parse/query.rb', line 1488 def fetch!(compiled_query) response = client.find_objects(@table, compiled_query.as_json, headers: _headers, **_opts) if response.error? puts "[ParseQuery] #{response.error}" end response end |
#first(limit = 1) ⇒ Parse::Object #first(constraints = {}) ⇒ Parse::Object
Supports all constraint options like :keys, :includes, :order, etc.
1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 |
# File 'lib/parse/query.rb', line 1286 def first(limit_or_constraints = 1, mongo_direct: false, **) # 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.
1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 |
# File 'lib/parse/query.rb', line 1957 def first_direct(limit_or_constraints = 1) if limit_or_constraints.is_a?(Hash) conditions(limit_or_constraints) limit_or_constraints = 1 end count = case limit_or_constraints when Numeric then limit_or_constraints.to_i when String unless limit_or_constraints =~ /\A-?\d+\z/ raise ArgumentError, "Invalid first_direct() argument #{limit_or_constraints.inspect}. " \ "Expected an Integer, a numeric String, or a Hash of constraints." end limit_or_constraints.to_i else raise ArgumentError, "Invalid first_direct() argument #{limit_or_constraints.inspect}. " \ "Expected an Integer, a numeric String, or a Hash of constraints." end count = 1 if count <= 0 # Set limit for single/few results original_limit = @limit @limit = count begin items = results_direct ensure @limit = original_limit end count == 1 ? items.first : items.first(count) end |
#get(object_id) ⇒ Parse::Object
Retrieve a single object by its objectId.
1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 |
# File 'lib/parse/query.rb', line 1373 def get(object_id) parse_class = Object.const_get(@table) if Object.const_defined?(@table) parse_class ||= Parse::Object response = client.fetch_object(@table, object_id) if response.error? raise Parse::Error.new(response.code, response.error) end Parse::Object.build(response.result, parse_class) end |
#get_pointer_target_class(field) ⇒ String?
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Get the target class name for a pointer field from model references. Uses the model's references hash which maps field names to target class names.
2561 2562 2563 2564 2565 2566 2567 2568 2569 2570 2571 2572 2573 2574 2575 2576 2577 2578 2579 2580 2581 2582 2583 2584 2585 |
# File 'lib/parse/query.rb', line 2561 def get_pointer_target_class(field) begin klass = Parse::Model.find_class(@table) return nil unless klass.respond_to?(:references) references = klass.references return nil if references.nil? || references.empty? # Check both the field name and its formatted Parse field name formatted_field = Query.format_field(field).to_sym # Try direct lookup first, then formatted field target = references[field] || references[formatted_field] # Also check field_map for aliased fields if target.nil? && klass.respond_to?(:field_map) mapped_field = klass.field_map[field] target = references[mapped_field] if mapped_field end target rescue NameError, StandardError nil end end |
#group_by(field, flatten_arrays: false, sortable: false, return_pointers: false, mongo_direct: false) ⇒ GroupBy, SortableGroupBy
Group results by a specific field and return a GroupBy object for chaining aggregations.
4110 4111 4112 4113 4114 4115 4116 4117 4118 4119 4120 |
# File 'lib/parse/query.rb', line 4110 def group_by(field, flatten_arrays: false, sortable: false, return_pointers: false, mongo_direct: false) if field.nil? || !field.respond_to?(:to_s) raise ArgumentError, "Invalid field name passed to `group_by`." end if sortable SortableGroupBy.new(self, field, flatten_arrays: flatten_arrays, return_pointers: return_pointers, mongo_direct: mongo_direct) else GroupBy.new(self, field, flatten_arrays: flatten_arrays, return_pointers: return_pointers, mongo_direct: mongo_direct) end end |
#group_by_date(field, interval, sortable: false, return_pointers: false, timezone: nil, mongo_direct: false) ⇒ GroupByDate, SortableGroupByDate
Group results by a date field at specified time intervals.
4270 4271 4272 4273 4274 4275 4276 4277 4278 4279 4280 4281 4282 4283 4284 |
# File 'lib/parse/query.rb', line 4270 def group_by_date(field, interval, sortable: false, return_pointers: false, timezone: nil, mongo_direct: false) if field.nil? || !field.respond_to?(:to_s) raise ArgumentError, "Invalid field name passed to `group_by_date`." end unless [:year, :month, :week, :day, :hour, :minute, :second].include?(interval.to_sym) raise ArgumentError, "Invalid interval. Must be one of: :year, :month, :week, :day, :hour, :minute, :second" end if sortable SortableGroupByDate.new(self, field, interval.to_sym, return_pointers: return_pointers, timezone: timezone, mongo_direct: mongo_direct) else GroupByDate.new(self, field, interval.to_sym, return_pointers: return_pointers, timezone: timezone, mongo_direct: mongo_direct) end end |
#group_objects_by(field, return_pointers: false) ⇒ Hash
Group Parse objects by a field value and return arrays of actual objects. Unlike group_by which uses aggregation for counts/sums, this fetches all objects and groups them in Ruby, returning the actual Parse object instances.
4144 4145 4146 4147 4148 4149 4150 4151 4152 4153 4154 4155 4156 4157 4158 4159 4160 4161 4162 4163 4164 4165 4166 4167 4168 4169 4170 4171 4172 4173 4174 4175 4176 4177 4178 4179 4180 4181 4182 4183 4184 4185 4186 |
# File 'lib/parse/query.rb', line 4144 def group_objects_by(field, return_pointers: false) if field.nil? || !field.respond_to?(:to_s) raise ArgumentError, "Invalid field name passed to `group_objects_by`." end # Fetch all objects that match the query objects = results(return_pointers: return_pointers) # Group objects by the specified field value grouped = {} objects.each do |obj| # Get the field value for grouping field_value = if obj.respond_to?(:attributes) # For Parse objects, try multiple field access patterns obj.attributes[field.to_s] || obj.attributes[Query.format_field(field).to_s] || (obj.respond_to?(field) ? obj.send(field) : nil) elsif obj.is_a?(Hash) # For raw JSON objects, try multiple field access patterns obj[field.to_s] || obj[Query.format_field(field).to_s] || obj[field.to_sym] || obj[Query.format_field(field).to_sym] else # Fallback - try to access as method obj.respond_to?(field) ? obj.send(field) : nil end # Handle nil field values group_key = field_value.nil? ? "null" : field_value # Convert Parse pointer values to readable format for grouping key if group_key.is_a?(Hash) && group_key["__type"] == "Pointer" group_key = "#{group_key["className"]}##{group_key["objectId"]}" end # Initialize array if this is the first object for this group grouped[group_key] ||= [] grouped[group_key] << obj end grouped end |
#has_subquery_constraints?(constraints) ⇒ Boolean
Check if constraints contain $inQuery or $notInQuery that need resolution
3755 3756 3757 3758 3759 3760 3761 3762 3763 3764 3765 3766 3767 3768 3769 |
# File 'lib/parse/query.rb', line 3755 def has_subquery_constraints?(constraints) return false unless constraints.is_a?(Hash) constraints.any? do |field, value| if value.is_a?(Hash) # Check for both string and symbol keys since constraints can come from # different sources (JSON parsing vs Ruby symbol keys) value.key?("$inQuery") || value.key?(:"$inQuery") || value.key?("$notInQuery") || value.key?(:"$notInQuery") || has_subquery_constraints?(value) else false end end end |
#include(*fields) ⇒ Object
alias for includes
827 828 829 |
# File 'lib/parse/query.rb', line 827 def include(*fields) includes(*fields) end |
#includes(*fields) ⇒ self
Set a list of Parse Pointer columns to be fetched for matching records.
You may chain multiple columns with the . operator.
814 815 816 817 818 819 820 821 822 823 824 |
# File 'lib/parse/query.rb', line 814 def includes(*fields) @includes ||= [] fields.flatten.each do |field| if field.nil? == false && field.respond_to?(:to_s) @includes.push Query.format_field(field).to_sym end end @includes.uniq! @results = nil if fields.count > 0 self # chaining end |
#keys(*fields) ⇒ self Also known as: select_fields
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.
605 606 607 608 609 610 611 612 613 614 615 |
# File 'lib/parse/query.rb', line 605 def keys(*fields) @keys ||= [] fields.flatten.each do |field| if field.nil? == false && field.respond_to?(:to_s) @keys.push Query.format_field(field).to_sym end end @keys.uniq! @results = nil if fields.count > 0 self # chaining end |
#last_updated(limit = 1, **options) ⇒ Parse::Object+
Supports all constraint options like :keys, :includes, :limit, etc.
Returns the most recently updated object(s) (ordered by updated_at descending).
1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 |
# File 'lib/parse/query.rb', line 1357 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).
1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 |
# File 'lib/parse/query.rb', line 1335 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.
757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 |
# File 'lib/parse/query.rb', line 757 def limit(count) case count when nil @limit = nil when Numeric @limit = [0, count.to_i].max when :max @limit = :max when String unless count =~ /\A-?\d+\z/ raise ArgumentError, "Invalid limit #{count.inspect}. Expected an Integer, :max, " \ "a numeric String, or nil." end @limit = [0, count.to_i].max else raise ArgumentError, "Invalid limit #{count.inspect}. Expected an Integer, :max, " \ "a numeric String, or nil." end @results = nil self #chaining end |
#map { ... } ⇒ Array
1258 1259 1260 1261 |
# File 'lib/parse/query.rb', line 1258 def map(&block) return results.enum_for(:map) unless block_given? # Sparkling magic! results.map(&block) end |
#max(field) ⇒ Object
Find the maximum value for a specific field.
4067 4068 4069 4070 4071 4072 4073 4074 4075 4076 4077 4078 4079 4080 4081 |
# File 'lib/parse/query.rb', line 4067 def max(field) if field.nil? || !field.respond_to?(:to_s) raise ArgumentError, "Invalid field name passed to `max`." end # Format field name according to Parse conventions formatted_field = format_aggregation_field(field) # Build the aggregation pipeline pipeline = [ { "$group" => { "_id" => nil, "max" => { "$max" => "$#{formatted_field}" } } }, ] execute_basic_aggregation(pipeline, "max", field, "max") end |
#min(field) ⇒ Object
Find the minimum value for a specific field.
4048 4049 4050 4051 4052 4053 4054 4055 4056 4057 4058 4059 4060 4061 4062 |
# File 'lib/parse/query.rb', line 4048 def min(field) if field.nil? || !field.respond_to?(:to_s) raise ArgumentError, "Invalid field name passed to `min`." end # Format field name according to Parse conventions formatted_field = format_aggregation_field(field) # Build the aggregation pipeline pipeline = [ { "$group" => { "_id" => nil, "min" => { "$min" => "$#{formatted_field}" } } }, ] execute_basic_aggregation(pipeline, "min", field, "min") end |
#not_publicly_readable(mongo_direct: nil) ⇒ Parse::Query
Find objects that are NOT publicly readable. Matches objects where _rperm does NOT contain "*".
5388 5389 5390 5391 5392 |
# File 'lib/parse/query.rb', line 5388 def not_publicly_readable(mongo_direct: nil) @acl_query_mongo_direct = mongo_direct unless mongo_direct.nil? where(:ACL.not_readable_by => "*") self end |
#not_publicly_writable(mongo_direct: nil) ⇒ Parse::Query
Find objects that are NOT publicly writable. Matches objects where _wperm does NOT contain "*".
5401 5402 5403 5404 5405 |
# File 'lib/parse/query.rb', line 5401 def not_publicly_writable(mongo_direct: nil) @acl_query_mongo_direct = mongo_direct unless mongo_direct.nil? where(:ACL.not_writable_by => "*") self end |
#or_where(where_clauses = []) ⇒ Query
Combine two where clauses into an OR constraint. Equivalent to the $or
Parse query operation. This is useful if you want to find objects that
match several queries. We overload the | operator in order to have a
clean syntax for joining these or operations.
977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 |
# File 'lib/parse/query.rb', line 977 def or_where(where_clauses = []) where_clauses = where_clauses.where if where_clauses.is_a?(Parse::Query) where_clauses = Parse::Query.new(@table, where_clauses).where if where_clauses.is_a?(Hash) return self if where_clauses.blank? # we can only have one compound query constraint. If we need to add another OR clause # let's find the one we have (if any) compound = @where.find { |f| f.is_a?(Parse::Constraint::CompoundQueryConstraint) } # create a set of clauses that are not an OR clause. remaining_clauses = @where.select { |f| f.is_a?(Parse::Constraint::CompoundQueryConstraint) == false } # if we don't have a OR clause to reuse, then create a new one with then # current set of constraints if compound.blank? initial_constraints = Parse::Query.compile_where(remaining_clauses) # Only include initial constraints if they're not empty initial_values = initial_constraints.empty? ? [] : [initial_constraints] compound = Parse::Constraint::CompoundQueryConstraint.new :or, initial_values end # then take the where clauses from the second query and append them. new_constraints = Parse::Query.compile_where(where_clauses) # Only add new constraints if they're not empty unless new_constraints.empty? compound.value.push new_constraints end #compound = Parse::Constraint::CompoundQueryConstraint.new :or, [remaining_clauses, or_where_query.where] @where = [compound] self #chaining end |
#order(*ordering) ⇒ self
Add a sorting order for the query.
678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 |
# File 'lib/parse/query.rb', line 678 def order(*ordering) @order ||= [] # Don't flatten through Hashes — flatten only unpacks Arrays. ordering.flatten.each do |entry| case entry when Order entry.field = Query.format_field(entry.field) @order.push entry when Symbol, String o = Order.new(entry) o.field = Query.format_field(o.field) @order.push o when Hash entry.each do |field, direction| dir_sym = direction.is_a?(String) ? direction.downcase.to_sym : direction unless dir_sym == :asc || dir_sym == :desc raise ArgumentError, "Invalid order direction #{direction.inspect} for field " \ "#{field.inspect}. Expected :asc or :desc." end o = Order.new(field, dir_sym) o.field = Query.format_field(o.field) @order.push o end else raise ArgumentError, "Invalid order argument #{entry.inspect}. Expected a Symbol, " \ "String, Parse::Order (e.g. :field.asc / :field.desc), or " \ "Hash of {field => :asc | :desc}." end end @results = nil if ordering.count > 0 self #chaining end |
#pipeline ⇒ Array
Returns the aggregation pipeline for this query if it contains pipeline-based constraints
3977 3978 3979 3980 3981 3982 3983 3984 3985 3986 3987 3988 3989 3990 3991 |
# File 'lib/parse/query.rb', line 3977 def pipeline pipeline_stages = [] # Check if any constraints generate aggregation pipelines @where.each do |constraint| if constraint.respond_to?(:as_json) constraint_json = constraint.as_json if constraint_json.is_a?(Hash) && constraint_json.has_key?("__aggregation_pipeline") pipeline_stages.concat(constraint_json["__aggregation_pipeline"]) end end end pipeline_stages end |
#pipeline_uses_internal_fields?(pipeline) ⇒ Boolean
Check if the pipeline references internal Parse fields that require MongoDB direct access
3514 3515 3516 3517 3518 |
# File 'lib/parse/query.rb', line 3514 def pipeline_uses_internal_fields?(pipeline) internal_fields = %w[_rperm _wperm _acl] pipeline_json = pipeline.to_json internal_fields.any? { |field| pipeline_json.include?(field) } end |
#pluck(field) ⇒ Array
Extract values for a specific field from all matching objects. This is similar to keys() but returns an array of the actual field values instead of objects with only those fields selected.
636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 |
# File 'lib/parse/query.rb', line 636 def pluck(field) if field.nil? || !field.respond_to?(:to_s) raise ArgumentError, "Invalid field name passed to `pluck`." end # Use keys to select only the field we want for efficiency query_with_field = self.dup.keys(field) # Get the results and extract the field values objects = query_with_field.results formatted_field = Query.format_field(field) objects.map do |obj| if obj.respond_to?(:attributes) # For Parse objects, get the attribute value obj.attributes[field.to_s] || obj.attributes[formatted_field.to_s] elsif obj.is_a?(Hash) # For raw JSON objects obj[field.to_s] || obj[formatted_field.to_s] else # Fallback - try to access as method obj.respond_to?(field) ? obj.send(field) : nil end end end |
#prepared(includeClassName: false) ⇒ Hash
Returns a compiled query without encoding the where clause.
3914 3915 3916 |
# File 'lib/parse/query.rb', line 3914 def prepared(includeClassName: false) compile(encode: false, includeClassName: includeClassName) end |
#pretty ⇒ String
Retruns a formatted JSON string representing the query, useful for debugging.
4001 4002 4003 |
# File 'lib/parse/query.rb', line 4001 def pretty JSON.pretty_generate(as_json) end |
#private_acl(mongo_direct: nil) ⇒ Parse::Query Also known as: master_key_only
Find objects with completely private ACL (no read AND no write permissions). Only accessible with master key.
5374 5375 5376 5377 |
# File 'lib/parse/query.rb', line 5374 def private_acl(mongo_direct: nil) privately_readable(mongo_direct: mongo_direct) privately_writable(mongo_direct: mongo_direct) end |
#privately_readable(mongo_direct: nil) ⇒ Parse::Query Also known as: master_key_read_only
Find objects with no read permissions (master key only). Matches objects where _rperm is empty or doesn't exist.
5346 5347 5348 |
# File 'lib/parse/query.rb', line 5346 def privately_readable(mongo_direct: nil) readable_by("none", mongo_direct: mongo_direct) end |
#privately_writable(mongo_direct: nil) ⇒ Parse::Query Also known as: master_key_write_only
Find objects with no write permissions (master key only). Matches objects where _wperm is empty or doesn't exist.
5360 5361 5362 |
# File 'lib/parse/query.rb', line 5360 def privately_writable(mongo_direct: nil) writable_by("none", mongo_direct: mongo_direct) end |
#publicly_readable(mongo_direct: nil) ⇒ Parse::Query
Find objects that are publicly readable (anyone can read). Matches objects where _rperm contains "*".
5322 5323 5324 |
# File 'lib/parse/query.rb', line 5322 def publicly_readable(mongo_direct: nil) readable_by("*", mongo_direct: mongo_direct) end |
#publicly_writable(mongo_direct: nil) ⇒ Parse::Query
Find objects that are publicly writable (anyone can write). Matches objects where _wperm contains "*". Useful for security audits to find potentially insecure objects.
5334 5335 5336 |
# File 'lib/parse/query.rb', line 5334 def publicly_writable(mongo_direct: nil) writable_by("*", mongo_direct: mongo_direct) end |
#raw { ... } ⇒ Array<Hash>
Returns raw unprocessed results from the query (hash format)
1843 1844 1845 |
# File 'lib/parse/query.rb', line 1843 def raw(&block) results(raw: true, &block) end |
#read_pref(preference) ⇒ self
Set the MongoDB read preference for this query. This allows directing read queries to secondary replicas for load balancing.
790 791 792 793 |
# File 'lib/parse/query.rb', line 790 def read_pref(preference) @read_preference = preference self end |
#readable_by(permission, mongo_direct: nil) ⇒ Parse::Query
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.
5250 5251 5252 5253 5254 |
# File 'lib/parse/query.rb', line 5250 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).
5265 5266 5267 5268 5269 |
# File 'lib/parse/query.rb', line 5265 def readable_by_role(role_name, mongo_direct: nil) @acl_query_mongo_direct = mongo_direct unless mongo_direct.nil? where(:ACL.readable_by_role => role_name) self end |
#related_to(field, pointer) ⇒ Object
795 796 797 798 799 |
# File 'lib/parse/query.rb', line 795 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
3995 3996 3997 |
# File 'lib/parse/query.rb', line 3995 def requires_aggregation? !pipeline.empty? end |
#requires_aggregation_pipeline? ⇒ Boolean
Check if this query contains constraints that require aggregation pipeline processing
1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 |
# File 'lib/parse/query.rb', line 1824 def requires_aggregation_pipeline? return false if @where.empty? # Markers (including __aggregation_pipeline) are stripped from the # public compile_where path; consult the marker view explicitly. markers = compile_markers # Check if the marker hash itself has aggregation pipeline marker return true if markers.key?("__aggregation_pipeline") # Check if any of the constraint values has aggregation pipeline marker markers.values.any? { |constraint| constraint.is_a?(Hash) && constraint.key?("__aggregation_pipeline") } end |
#requires_mongo_direct? ⇒ Boolean
Check if this query contains a constraint that can only be answered
via mongo-direct (e.g. $geoIntersects with a full $geometry
against a non-GeoPoint column — an operator Parse Server's REST
find layer does not expose). Direct-only constraints emit a
"__mongo_direct_only" marker which this predicate detects.
1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 |
# File 'lib/parse/query.rb', line 1597 def requires_mongo_direct? return false if @where.empty? # Read from the un-stripped marker hash — `compile_where` removes # `__`-prefixed routing markers before they ship to Parse / Mongo. markers = compile_markers return true if markers.key?("__mongo_direct_only") markers.values.any? do |constraint| constraint.is_a?(Hash) && constraint.key?("__mongo_direct_only") end end |
#result_pointers { ... } ⇒ Array<Parse::Pointer> Also known as: results_pointers
Returns only pointer objects for all matching results This is memory efficient for large result sets where you only need pointers
1851 1852 1853 |
# File 'lib/parse/query.rb', line 1851 def result_pointers(&block) results(return_pointers: true, &block) end |
#results(raw: false, return_pointers: false, mongo_direct: false) { ... } ⇒ Array<Hash>, Array<Parse::Object> Also known as: result
Executes the query and builds the result set of Parse::Objects that matched. When this method is passed a block, the block is yielded for each matching item in the result, and the items are not returned. This methodology is more performant as large quantifies of objects are fetched in batches and all of them do not have to be kept in memory after the query finishes executing. This is the recommended method of processing large result sets.
1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 |
# File 'lib/parse/query.rb', line 1521 def results(raw: false, return_pointers: false, mongo_direct: false, &block) # Use direct MongoDB query if requested if mongo_direct return results_direct(raw: raw, **mongo_direct_auth_kwargs, &block) end # Auto-route to mongo-direct when the compiled where contains a # constraint that Parse Server's REST find layer cannot express # (e.g. $geoIntersects with a full $geometry against a non-Point # column). Mirrors the existing aggregation auto-route at line # ~1321 below — the constraint emits a marker, the query layer # detects it, and routing happens transparently. The auth # context (use_master_key, scope_to_user, or session_token) # decides how ACL simulation runs through mongo-direct. if requires_mongo_direct? assert_mongo_direct_routable! return results_direct(raw: raw, **mongo_direct_auth_kwargs, &block) end if @results.nil? if block_given? max_results(raw: raw, return_pointers: return_pointers, &block) elsif @limit.is_a?(Numeric) || requires_aggregation_pipeline? # Check if this query requires aggregation pipeline processing if requires_aggregation_pipeline? # Use Aggregation class which handles both Parse Server and MongoDB direct aggregation = execute_aggregation_pipeline if raw items = aggregation.raw elsif return_pointers items = to_pointers(aggregation.raw) else items = aggregation.results end return items.each(&block) if block_given? @results = items else response = fetch!(compile) return [] if response.error? items = if raw response.results elsif return_pointers to_pointers(response.results) else decode(response.results) end return items.each(&block) if block_given? @results = items end else @results = max_results(raw: raw, return_pointers: return_pointers) end end @results end |
#results_direct(raw: false, max_time_ms: nil, session_token: nil, master: nil, acl_user: nil, acl_role: nil) { ... } ⇒ Array<Parse::Object>, Array<Hash>
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.
1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 |
# File 'lib/parse/query.rb', line 1880 def results_direct(raw: false, max_time_ms: nil, session_token: nil, master: nil, acl_user: nil, acl_role: nil, &block) require_relative "mongodb" Parse::MongoDB.require_gem! unless Parse::MongoDB.available? raise Parse::MongoDB::NotEnabled, "Direct MongoDB queries are not enabled. " \ "Call Parse::MongoDB.configure(uri: 'mongodb://...', enabled: true) first." end # Build the aggregation pipeline for direct MongoDB execution pipeline = build_direct_mongodb_pipeline # When no explicit auth kwargs are provided by the caller, derive them # from the query's own auth state (session_token, acl_user, acl_role, or # master key) via mongo_direct_auth_kwargs — exactly the same fallback # used by distinct_direct, count_direct, and the requires_mongo_direct? # auto-route in results(). Without this, a plain .results_direct call on # a master-key client would resolve as anonymous and have the ACL match # stage filter out every row whose _rperm is [] (the default for objects # created without an explicit public-read ACL). if session_token.nil? && master.nil? && acl_user.nil? && acl_role.nil? auth = mongo_direct_auth_kwargs session_token = auth[:session_token] master = auth[:master] acl_user = auth[:acl_user] acl_role = auth[:acl_role] end # Execute the aggregation directly on MongoDB. The pipeline was built # entirely from SDK constraint translation (no user-supplied stages), # so legitimate +_rperm+/+_wperm+ references emitted by # {#readable_by_role} and friends are sanctioned. The DENIED_OPERATORS # walk still runs at the MongoDB layer. When `session_token:` or # `master:` is supplied, Parse::MongoDB.aggregate adds the # three-layer ACL simulation (top-level $match, $lookup rewriter, # post-fetch redactor) before/after the pipeline executes. raw_results = Parse::MongoDB.aggregate(@table, pipeline, max_time_ms: max_time_ms, allow_internal_fields: true, session_token: session_token, master: master, acl_user: acl_user, acl_role: acl_role, read_preference: @read_preference) # Convert MongoDB documents to Parse format parse_results = Parse::MongoDB.convert_documents_to_parse(raw_results, @table) if raw return parse_results.each(&block) if block_given? return parse_results end # Convert to Parse objects items = decode(parse_results) return items.each(&block) if block_given? items end |
#rewrite_expression_for_direct_mongodb(expr) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Recursively rewrite field references inside an aggregation expression to their direct-MongoDB column names.
Walks Strings, Arrays, and Hashes:
- A String starting with
$(but not$$, which denotes 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.
2940 2941 2942 2943 2944 2945 2946 2947 2948 2949 2950 2951 2952 2953 2954 2955 2956 2957 2958 2959 2960 2961 2962 2963 2964 |
# File 'lib/parse/query.rb', line 2940 def rewrite_expression_for_direct_mongodb(expr) case expr when String return expr unless expr.start_with?("$") # $$varName (let bindings) and $$ROOT / $$CURRENT / $$NOW etc. return expr if expr.start_with?("$$") # Split off the root path segment so `$user.name` rewrites only # the root: `$_p_user.name`. Internal helper handles _p_* and # built-in passthroughs idempotently. head, sep, tail = expr[1..-1].partition(".") "$#{convert_field_for_direct_mongodb(head)}#{sep}#{tail}" when Array expr.map { |e| rewrite_expression_for_direct_mongodb(e) } when Hash result = {} expr.each do |k, v| # `$literal` wraps a string constant; its argument is not a # field reference and must be preserved verbatim. result[k] = k.to_s == "$literal" ? v : rewrite_expression_for_direct_mongodb(v) end result else expr end end |
#scope_to_role(role) ⇒ self
Role-based ACL scoping for service-account-style queries that
need "what would a user holding this role see" without minting a
session token or naming a specific user. The SDK uses
Parse::Role#all_parent_role_names to expand the role's
inheritance chain so passing "scope:admin" includes any role
"scope:admin" inherits from (e.g. "scope:user").
The resulting permission set is ["*", "role:<name>", ...] —
no user_id slot. Documents whose _rperm would only grant a
specific user (and not any of the role names) are filtered out
of both the top-level result set and embedded sub-documents.
Same routing rules as #scope_to_user: the query auto-routes
through mongo-direct when the where clause contains a
direct-only constraint, and the three-layer ACL simulation
(top-level $match, $lookup rewriter, post-fetch redactor)
runs through ACLScope.
1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 |
# File 'lib/parse/query.rb', line 1693 def scope_to_role(role) unless role.is_a?(Parse::Role) || role.is_a?(String) || role.is_a?(Symbol) raise ArgumentError, "[Parse::Query] scope_to_role requires a Parse::Role or role-name String." end # Normalize Symbol at the boundary so downstream # Parse::ACLScope#resolve_for_role only ever sees Parse::Role or # String. Without normalization, any String-only operation # (e.g. #start_with?, #sub) silently NoMethodErrors on Symbol. @acl_role = role.is_a?(Symbol) ? role.to_s : role self end |
#scope_to_user(user) ⇒ self
Scope a query to a specific user's row-level ACL when it auto-routes
through mongo-direct. The SDK records the user, computes the
effective _rperm allow-set (user objectId + "*" + every role
name the user inherits via Role.all_for_user), and prepends
a { _rperm: { $in: ... } } $match to the mongo-direct pipeline
at execution time.
What this does NOT replicate: class-level permissions (CLP),
anonymous-user public-access nuances, beforeFind/afterFind
cloud triggers, or any field-level redaction Parse Server might
otherwise apply. This is a row-ACL floor, not full enforcement
parity with the Parse Server REST path. The intended use case is
"I need this mongo-direct-only query from a session-tokened
context, and I accept the row-ACL floor as my filter."
Edge case — objects with missing _rperm: Parse Server only
writes _rperm when an explicit ACL is applied; rows saved with
master-key access and no explicit ACL leave the field unset.
The injected filter is {$or: [{_rperm: {$exists: false}},
{_rperm: {$in: perms}}]}, treating missing-_rperm rows as
public-readable. Apps that store row-level ACL on every object
are unaffected by this fallback; apps that mix ACL'd and
public-default rows will see both classes of row through the
scoped query.
The query MUST still satisfy #assert_mongo_direct_routable! —
either use_master_key: true OR scope_to_user is set. A call to
scope_to_user is treated as opt-in to mongo-direct routing for
the direct-only constraints in the where clause.
1658 1659 1660 1661 1662 1663 |
# File 'lib/parse/query.rb', line 1658 def scope_to_user(user) raise ArgumentError, "[Parse::Query] scope_to_user requires a Parse::User or User Pointer." \ unless user.respond_to?(:id) && user.id.is_a?(String) @acl_user = user self end |
#select { ... } ⇒ Array
1266 1267 1268 1269 |
# File 'lib/parse/query.rb', line 1266 def select(&block) return results.enum_for(:select) unless block_given? # Sparkling magic! results.select(&block) end |
#skip(amount) ⇒ self
Use with limit to paginate through results. Default is 0.
719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 |
# File 'lib/parse/query.rb', line 719 def skip(amount) coerced = case amount when nil then 0 when Numeric then amount.to_i when String unless amount =~ /\A-?\d+\z/ raise ArgumentError, "Invalid skip #{amount.inspect}. Expected an Integer, " \ "a numeric String, or nil." end amount.to_i else raise ArgumentError, "Invalid skip #{amount.inspect}. Expected an Integer, " \ "a numeric String, or nil." end @skip = [0, coerced].max @results = nil self #chaining end |
#subscribe(fields: nil, session_token: nil, client: nil, use_master_key: false) {|subscription| ... } ⇒ Parse::LiveQuery::Subscription
Subscribe to real-time updates for objects matching this query. Uses Parse LiveQuery WebSocket connection to receive push notifications when objects are created, updated, deleted, or enter/leave the query results.
3035 3036 3037 3038 3039 3040 3041 3042 3043 3044 3045 3046 3047 |
# File 'lib/parse/query.rb', line 3035 def subscribe(fields: nil, session_token: nil, client: nil, use_master_key: false, &block) require_relative "live_query" lq_client = client || Parse::LiveQuery.client lq_client.subscribe( @table, where: compile_where, fields: fields, session_token: session_token || @session_token, use_master_key: use_master_key, &block ) end |
#sum(field) ⇒ Numeric
Calculate the sum of values for a specific field.
4008 4009 4010 4011 4012 4013 4014 4015 4016 4017 4018 4019 4020 4021 4022 |
# File 'lib/parse/query.rb', line 4008 def sum(field) if field.nil? || !field.respond_to?(:to_s) raise ArgumentError, "Invalid field name passed to `sum`." end # Format field name according to Parse conventions formatted_field = format_aggregation_field(field) # Build the aggregation pipeline pipeline = [ { "$group" => { "_id" => nil, "total" => { "$sum" => "$#{formatted_field}" } } }, ] execute_basic_aggregation(pipeline, "sum", field, "total") end |
#to_pointers(list, field = nil) ⇒ Array<Parse::Pointer>
Builds Parse::Pointer objects based on the set of Parse JSON hashes in an array.
3868 3869 3870 3871 3872 3873 3874 3875 3876 3877 3878 3879 3880 3881 3882 3883 3884 3885 3886 3887 3888 3889 3890 3891 3892 3893 3894 3895 3896 3897 3898 3899 3900 3901 3902 3903 |
# File 'lib/parse/query.rb', line 3868 def to_pointers(list, field = nil) list.map do |m| if field # Use schema-based conversion when field is provided converted = convert_pointer_value_with_schema(m, field, return_pointers: true) if converted.is_a?(Parse::Pointer) converted elsif m.is_a?(String) && m.include?("$") # Fallback to string parsing if schema conversion didn't work class_name, object_id = m.split("$", 2) if class_name && object_id Parse::Pointer.new(class_name, object_id) end else nil end else # Original logic for backward compatibility if m.is_a?(Hash) if m["__type"] == "Pointer" && m["className"] && m["objectId"] # Parse pointer object - use the className from the pointer Parse::Pointer.new(m["className"], m["objectId"]) elsif m["objectId"] # Standard Parse object with objectId - use the query table name Parse::Pointer.new(@table, m["objectId"]) end elsif m.is_a?(String) && m.include?("$") # Handle MongoDB pointer string format: "ClassName$objectId" class_name, object_id = m.split("$", 2) if class_name && object_id Parse::Pointer.new(class_name, object_id) end end end end.compact end |
#to_table(columns = nil, format: :ascii, headers: nil, sort_by: nil, sort_order: :asc) ⇒ String
Convert query results to a formatted table display.
4223 4224 4225 4226 4227 4228 4229 4230 4231 4232 4233 4234 4235 4236 4237 4238 4239 4240 4241 4242 4243 4244 4245 4246 4247 4248 4249 4250 4251 |
# File 'lib/parse/query.rb', line 4223 def to_table(columns = nil, format: :ascii, headers: nil, sort_by: nil, sort_order: :asc) objects = results return format_empty_table(format) if objects.empty? # Auto-detect columns if not provided if columns.nil? columns = auto_detect_columns(objects.first) end # Build table data table_data = build_table_data(objects, columns, headers) # Sort table data if sort_by is specified if sort_by sort_table_data!(table_data, sort_by, sort_order) end # Format based on requested format case format when :ascii format_ascii_table(table_data) when :csv format_csv_table(table_data) when :json format_json_table(table_data) else raise ArgumentError, "Unsupported format: #{format}. Use :ascii, :csv, or :json" end end |
#translate_pipeline_for_direct_mongodb(pipeline) ⇒ Array<Hash>
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Apply the direct-MongoDB stage converter to every stage in a pipeline.
Idempotent on already-translated input (the per-stage converter
passes _p_* references through unchanged).
3287 3288 3289 3290 |
# File 'lib/parse/query.rb', line 3287 def translate_pipeline_for_direct_mongodb(pipeline) return pipeline unless pipeline.is_a?(Array) pipeline.map { |stage| convert_stage_for_direct_mongodb(stage) } end |
#validate_no_where_operator!(hash) ⇒ Object
Retained for backwards compatibility. Use PipelineSecurity.validate_filter! for new code.
3318 3319 3320 3321 3322 |
# File 'lib/parse/query.rb', line 3318 def validate_no_where_operator!(hash) Parse::PipelineSecurity.validate_filter!(hash) rescue Parse::PipelineSecurity::Error => e raise ArgumentError, e. 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.
3308 3309 3310 3311 3312 |
# File 'lib/parse/query.rb', line 3308 def validate_pipeline!(pipeline) Parse::PipelineSecurity.validate_filter!(pipeline) rescue Parse::PipelineSecurity::Error => e raise ArgumentError, e. 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.
952 953 954 955 956 957 958 959 |
# File 'lib/parse/query.rb', line 952 def where(expressions = nil, opts = {}) return @where if expressions.nil? if expressions.is_a?(Hash) # Route through conditions to handle special keywords like :keys, :include, etc. conditions(expressions) end self #chaining end |
#where_constraints ⇒ Hash
Formats the current set of Parse::Constraint instances in the where clause as an expression hash.
934 935 936 |
# File 'lib/parse/query.rb', line 934 def where_constraints @where.reduce({}) { |memo, constraint| memo[constraint.operation] = constraint.value; memo } end |
#writable_by(permission, mongo_direct: nil) ⇒ Parse::Query
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.
5289 5290 5291 5292 5293 |
# File 'lib/parse/query.rb', line 5289 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).
5304 5305 5306 5307 5308 |
# File 'lib/parse/query.rb', line 5304 def writable_by_role(role_name, mongo_direct: nil) @acl_query_mongo_direct = mongo_direct unless mongo_direct.nil? where(:ACL.writable_by_role => role_name) self end |
#|(other_query) ⇒ Query
Returns the combined query with an OR clause.
1007 1008 1009 1010 1011 1012 |
# File 'lib/parse/query.rb', line 1007 def |(other_query) raise ArgumentError, "Parse queries must be of the same class #{@table}." unless @table == other_query.table copy_query = self.clone copy_query.or_where other_query.where copy_query end |