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
- HINT_UNSET =
Set a MongoDB index hint for this query. Forces Parse Server (and the underlying MongoDB driver) to use the named index instead of the query planner's choice. Useful for benchmarking or for working around sub-optimal plan selection. The hint is emitted in the compiled REST query body as the +hint+ parameter (supported by Parse Server 7.4.0+) AND forwarded to the mongo-direct path — +results_direct+ / +count_direct+ / +distinct_direct+ pass it to MongoDB.aggregate/MongoDB.find as the Mongo +hint+ option, so a plan diagnosed with #explain can be corrected on either path.
:_hint_unset_- RESERVED_EXCLUDE_KEYS =
Reserved fields that #redact_excluded_keys! never strips: dropping these would break #decode (objectId / className / __type) or remove the required Parse envelope. Both the Parse-format names (objectId, createdAt, updatedAt, ACL) and their Mongo storage-form counterparts (_id, _created_at, _updated_at, _acl) are guarded, so the redaction is safe even if it is ever pointed at a raw Mongo document, and a caller can't break reconstruction by excluding e.g.
:_id. This is an SDK safety choice, not an assertion about which fields Parse Server's RESTexcludeKeysstrips. %w[ objectId className __type createdAt updatedAt ACL _id _created_at _updated_at _acl ].freeze
- BLOCKED_PIPELINE_STAGES =
Deprecated.
Retained for backwards compatibility. The canonical list now lives in PipelineSecurity::DENIED_OPERATORS and is enforced recursively, not only at the top-level stage.
Create an Aggregation object for executing arbitrary MongoDB pipelines Pipeline stages that are blocked to prevent data exfiltration or destructive operations.
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) ⇒ Object
-
.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, raw_values: false, raw_field_names: false) ⇒ 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
-
#exclude_keys(*fields) ⇒ self
Set a server-side field denylist for this query.
-
#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, keys: nil, watch: nil, session_token: nil, client: nil, use_master_key: false) {|subscription| ... } ⇒ Parse::LiveQuery::Subscription
Subscribe to real-time updates for objects matching this query.
-
#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.
488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 |
# File 'lib/parse/query.rb', line 488 def initialize(table, constraints = {}) table = table.to_s.to_parse_class if table.is_a?(Symbol) table = table.parse_class if table.respond_to?(:parse_class) raise ArgumentError, "First parameter should be the name of the Parse class (table)" unless table.is_a?(String) @count = 0 #non-zero/1 implies a count query request @where = [] @order = [] @keys = [] @exclude_keys = [] @includes = [] @limit = nil @skip = 0 @table = table @cache = Parse.default_query_cache # Tri-state: `nil` means "no caller preference" — the request layer # then applies the master-key default, the `Parse.client_mode` flag, # and the `Parse.with_session` ambient as configured. Explicit # `true` / `false` (set via `use_master_key=` or the `use_master_key:` # constraint key) wins over both. A `true` default here would # silently smuggle the master-key header past every client-mode # query, so we deliberately leave the decision to the request layer # unless the caller said otherwise. @use_master_key = nil @verbose_aggregate = false @hint = nil conditions constraints end |
Class Attribute Details
.allow_scope_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.
1896 1897 1898 |
# File 'lib/parse/query.rb', line 1896 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.
1892 1893 1894 |
# File 'lib/parse/query.rb', line 1892 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
5388 5389 5390 5391 5392 5393 5394 5395 5396 5397 5398 5399 5400 5401 5402 5403 5404 5405 5406 5407 5408 5409 5410 5411 5412 5413 5414 5415 5416 5417 5418 5419 |
# File 'lib/parse/query.rb', line 5388 def self.and(*queries) queries = queries.flatten.compact return nil if queries.empty? # Get the table from the first query table = queries.first.table # Ensure all queries are for the same table unless queries.all? { |q| q.table == table } raise ArgumentError, "All queries passed to Parse::Query.and must be for the same Parse class." end # Start with an empty query for this table result = self.new(table) # Filter to only queries that have constraints queries = queries.filter { |q| q.where.present? && !q.where.empty? } # Add each query's complete constraint set with AND logic # Multiple constraints in a query are implicitly ANDed together by Parse queries.each do |query| # Compile the where constraints to check if they result in empty conditions compiled_where = Parse::Query.compile_where(query.where) unless compiled_where.empty? # Directly append constraints to result's where array # (where method only accepts Hash, but query.where returns Array<Constraint>) result.instance_variable_get(:@where).concat(query.where) end end result end |
.compile_markers(where) ⇒ Hash
Return the un-stripped reduced hash so the routing/pipeline layer
can inspect __-prefixed markers (e.g. "__mongo_direct_only",
"__aggregation_pipeline"). These markers are SDK-internal hints
and must never be sent to Parse REST or MongoDB — that's what
compile_where is for.
397 398 399 |
# File 'lib/parse/query.rb', line 397 def compile_markers(where) constraint_reduce(where) end |
.compile_where(where) ⇒ Object
386 387 388 |
# File 'lib/parse/query.rb', line 386 def compile_where(where) constraint_reduce(where).reject { |k, _| k.is_a?(String) && k.start_with?("__") } end |
.format_field(str) ⇒ String
Returns formatted string using field_formatter.
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
5353 5354 5355 5356 5357 5358 5359 5360 5361 5362 5363 5364 5365 5366 5367 5368 5369 5370 5371 5372 5373 5374 5375 5376 5377 5378 5379 5380 5381 |
# File 'lib/parse/query.rb', line 5353 def self.or(*queries) queries = queries.flatten.compact return nil if queries.empty? # Get the table from the first query table = queries.first.table # Ensure all queries are for the same table unless queries.all? { |q| q.table == table } raise ArgumentError, "All queries passed to Parse::Query.or must be for the same Parse class." end # Start with an empty query for this table result = self.new(table) # Filter to only queries that have constraints queries = queries.filter { |q| q.where.present? && !q.where.empty? } # Add each query's complete constraint set as an OR branch queries.each do |query| # Compile the where constraints to check if they result in empty conditions compiled_where = Parse::Query.compile_where(query.where) unless compiled_where.empty? result.or_where(query.where) end end result end |
.parse_keys_to_nested_keys(keys) ⇒ Hash
Parses keys patterns to build a map of nested fetched keys. Handles arbitrary nesting depth (e.g., "a.b.c.d" creates entries for a, b, c). For example, ["project.name", "project.status", "author.email"] becomes: { project: [:name, :status], author: [:email] }
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.
940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 |
# File 'lib/parse/query.rb', line 940 def add_constraint(operator, value = nil, opts = {}) @where ||= [] constraint = operator # assume Parse::Constraint unless constraint.is_a?(Parse::Constraint) constraint = Parse::Constraint.create(operator, value) end return unless constraint.is_a?(Parse::Constraint) # to support select queries where you have to pass a `key` parameter for matching # different tables. if constraint.operand == :key || constraint.operand == "key" @key = constraint.value return end unless opts[:filter] == false constraint.operand = Query.format_field(constraint.operand) end reject_vector_constraint!(constraint) @where.push constraint @results = nil self #chaining end |
#add_constraints(list) ⇒ self
Combine a list of Constraint objects
918 919 920 921 922 |
# File 'lib/parse/query.rb', line 918 def add_constraints(list) list = Array.wrap(list).select { |m| m.is_a?(Parse::Constraint) } @where = @where + list self end |
#after_prepare { ... } ⇒ Object
A callback called after the query is compiled
129 |
# File 'lib/parse/query.rb', line 129 define_model_callbacks :prepare, only: [:after, :before] |
#aggregate(pipeline, verbose: nil, mongo_direct: nil, rewrite_lookups: nil, raw_values: false, raw_field_names: false) ⇒ Object Also known as: aggregate_pipeline
3342 3343 3344 3345 3346 3347 3348 3349 3350 3351 3352 3353 3354 3355 3356 3357 3358 3359 3360 3361 3362 3363 3364 3365 3366 3367 3368 3369 3370 3371 3372 3373 3374 3375 3376 3377 3378 3379 3380 3381 3382 3383 3384 3385 3386 3387 3388 3389 3390 3391 3392 3393 3394 3395 3396 3397 3398 3399 3400 3401 3402 3403 3404 3405 3406 3407 3408 3409 3410 3411 3412 3413 3414 3415 3416 3417 3418 3419 3420 3421 3422 3423 3424 3425 3426 3427 3428 3429 3430 3431 3432 3433 3434 3435 3436 3437 3438 3439 3440 3441 3442 3443 3444 3445 3446 3447 3448 3449 3450 3451 3452 3453 3454 3455 3456 3457 3458 3459 3460 3461 3462 3463 3464 3465 3466 3467 3468 3469 3470 3471 3472 3473 3474 3475 3476 3477 3478 3479 3480 3481 3482 3483 3484 3485 3486 3487 3488 |
# File 'lib/parse/query.rb', line 3342 def aggregate(pipeline, verbose: nil, mongo_direct: nil, rewrite_lookups: nil, raw_values: false, raw_field_names: false) validate_pipeline!(pipeline) # Auto-rewrite LLM-style $lookup stages against logical Parse class # names into the Parse-on-Mongo column form (_p_*/parseReference) when # the foreign class declares parse_reference. Idempotent on already- # rewritten input. Controlled by Parse.rewrite_lookups (default true) # or the per-call `rewrite_lookups:` kwarg. pipeline = Parse::LookupRewriter.auto_rewrite( pipeline, class_name: @table, enabled: rewrite_lookups, ) # Automatically prepend query constraints as pipeline stages complete_pipeline = [] lookup_stages = [] # Track if we have $inQuery constraints # Add $match stage from where constraints if any exist unless @where.empty? # `compile_where` is marker-free; `compile_markers` carries the # __aggregation_pipeline stages we need to extract below. where_clause = compile_where markers = compile_markers if where_clause.any? || markers.key?("__aggregation_pipeline") # Collect match conditions and stages initial_match_conditions = [] aggregation_match_conditions = [] non_match_stages = [] post_lookup_match = {} # `where_clause` is already marker-free; treat as regular constraints. regular_constraints = where_clause if regular_constraints.any? # Handle dates first date_converted = convert_dates_for_aggregation(regular_constraints) # Extract $inQuery/$notInQuery and convert to $lookup stages if has_subquery_constraints?(date_converted) lookup_result = extract_subquery_to_lookup_stages(date_converted) date_converted = lookup_result[:constraints] lookup_stages = lookup_result[:lookup_stages] post_lookup_match = lookup_result[:post_lookup_match] end # Convert field names for aggregation context and handle pointers if date_converted.any? match_stage = convert_constraints_for_aggregation(date_converted) initial_match_conditions << match_stage end end # Extract aggregation pipeline stages from the marker view. if markers.key?("__aggregation_pipeline") markers["__aggregation_pipeline"].each do |stage| if stage.is_a?(Hash) && stage.key?("$match") aggregation_match_conditions << stage["$match"] else non_match_stages << stage end end end # Stage 1: Initial $match with regular constraints if initial_match_conditions.any? if initial_match_conditions.length == 1 complete_pipeline << { "$match" => initial_match_conditions.first } else complete_pipeline << { "$match" => { "$and" => initial_match_conditions } } end end # Stage 2: $lookup stages for subqueries ($addFields, $lookup) if lookup_stages.any? lookup_stages.each do |stage| next if stage.key?("$project") complete_pipeline << stage end # Stage 3: Post-lookup $match if post_lookup_match.any? complete_pipeline << { "$match" => post_lookup_match } end # Note: Skip cleanup $project stage - see build_aggregation_pipeline for reasoning end # Stage 5: Aggregation $match conditions if aggregation_match_conditions.any? if aggregation_match_conditions.length == 1 complete_pipeline << { "$match" => aggregation_match_conditions.first } else complete_pipeline << { "$match" => { "$and" => aggregation_match_conditions } } end end # Stage 6: Non-$match stages from aggregation pipeline complete_pipeline.concat(non_match_stages) end end # Append the provided pipeline stages complete_pipeline.concat(pipeline) # Add $sort stage from order constraints if any exist unless @order.empty? sort_stage = {} @order.each do |order_obj| # order_obj is a Parse::Order object with field and direction field_name = order_obj.field.to_s direction = order_obj.direction == :desc ? -1 : 1 sort_stage[field_name] = direction end complete_pipeline << { "$sort" => sort_stage } if sort_stage.any? end # Add $skip stage if specified if @skip > 0 complete_pipeline << { "$skip" => @skip } end # Add $limit stage if specified if @limit.is_a?(Numeric) && @limit > 0 complete_pipeline << { "$limit" => @limit } end # Auto-detect if mongo_direct is needed (when $inQuery constraints are present and MongoDB is available) use_mongo_direct = mongo_direct if use_mongo_direct.nil? && lookup_stages && lookup_stages.any? && defined?(Parse::MongoDB) && Parse::MongoDB.enabled? use_mongo_direct = true end # Optimize pipeline by merging consecutive $match stages complete_pipeline = deduplicate_consecutive_match_stages(complete_pipeline) # When the pipeline is bound for direct MongoDB, translate every stage # through the direct-MongoDB field rewriter so user-supplied stages # (which use logical Parse field names like `$author`) reach the # correct on-disk columns (`$_p_author`). The Parse Server route does # not need this — Parse Server applies its own translation on the # aggregate endpoint — so the rewrite is gated on use_mongo_direct. if use_mongo_direct complete_pipeline = translate_pipeline_for_direct_mongodb(complete_pipeline) end Aggregation.new(self, complete_pipeline, verbose: verbose, mongo_direct: use_mongo_direct || false, raw_values: raw_values, raw_field_names: raw_field_names) end |
#aggregate_from_query(additional_stages = [], verbose: nil, mongo_direct: nil) ⇒ Aggregation
Converts the current query into an aggregate pipeline and executes it. This method automatically converts all query constraints (where, order, limit, skip, etc.) into MongoDB aggregation pipeline stages.
3549 3550 3551 3552 3553 3554 3555 3556 3557 3558 3559 3560 3561 3562 3563 3564 |
# File 'lib/parse/query.rb', line 3549 def aggregate_from_query(additional_stages = [], verbose: nil, mongo_direct: nil) # Build pipeline from current query constraints pipeline, has_lookup_stages = build_query_aggregate_pipeline # Append any additional stages pipeline.concat(additional_stages) if additional_stages.any? # Auto-detect if mongo_direct is needed (when $inQuery constraints are present and MongoDB is available) use_mongo_direct = mongo_direct if use_mongo_direct.nil? && has_lookup_stages && defined?(Parse::MongoDB) && Parse::MongoDB.enabled? use_mongo_direct = true end # Create Aggregation directly to avoid double-applying constraints Aggregation.new(self, pipeline, verbose: verbose, mongo_direct: use_mongo_direct || false) end |
#all(expressions = { limit: :max }) { ... } ⇒ Array<Hash>, Array<Parse::Object>
Similar to #results but takes an additional set of conditions to apply. This method helps support the use of class and instance level scopes.
3989 3990 3991 3992 3993 |
# File 'lib/parse/query.rb', line 3989 def all(expressions = { limit: :max }, &block) conditions(expressions) return results(&block) if block_given? results end |
#as_json(*args) ⇒ Hash
4115 4116 4117 |
# File 'lib/parse/query.rb', line 4115 def as_json(*args) compile.as_json end |
#atlas_autocomplete(query, field:, **options) ⇒ Parse::AtlasSearch::AutocompleteResult
Execute an autocomplete search using MongoDB Atlas Search. Provides search-as-you-type functionality for a specific field.
2503 2504 2505 2506 2507 2508 2509 2510 2511 2512 2513 2514 2515 2516 2517 2518 2519 2520 2521 2522 2523 2524 2525 2526 2527 2528 2529 |
# File 'lib/parse/query.rb', line 2503 def atlas_autocomplete(query, field:, **) 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.
2559 2560 2561 2562 2563 2564 2565 2566 2567 2568 2569 2570 2571 2572 2573 2574 2575 2576 2577 2578 2579 2580 2581 2582 2583 2584 2585 2586 |
# File 'lib/parse/query.rb', line 2559 def atlas_facets(query, facets, **) 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.
2393 2394 2395 2396 2397 2398 2399 2400 2401 2402 2403 2404 2405 2406 2407 2408 2409 2410 2411 2412 2413 2414 2415 2416 2417 2418 2419 2420 2421 2422 2423 2424 2425 2426 2427 2428 2429 2430 2431 2432 2433 2434 2435 2436 2437 2438 2439 2440 2441 2442 2443 2444 2445 2446 2447 2448 2449 2450 2451 2452 2453 2454 2455 2456 2457 2458 2459 2460 2461 2462 2463 2464 2465 2466 2467 2468 2469 2470 2471 2472 2473 |
# File 'lib/parse/query.rb', line 2393 def atlas_search(query = nil, **, &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, hint: @hint) # 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.
4248 4249 4250 4251 4252 4253 4254 4255 4256 4257 4258 4259 4260 4261 4262 |
# File 'lib/parse/query.rb', line 4248 def average(field) if field.nil? || !field.respond_to?(:to_s) raise ArgumentError, "Invalid field name passed to `average`." end # Format field name according to Parse conventions formatted_field = format_aggregation_field(field) # Build the aggregation pipeline pipeline = [ { "$group" => { "_id" => nil, "avg" => { "$avg" => "$#{formatted_field}" } } }, ] execute_basic_aggregation(pipeline, "average", field, "avg") end |
#before_prepare { ... } ⇒ Object
A callback called before the query is compiled
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
3732 3733 3734 3735 3736 3737 3738 3739 3740 3741 3742 3743 3744 3745 3746 3747 3748 3749 3750 3751 3752 3753 3754 3755 3756 3757 3758 3759 3760 3761 3762 3763 3764 3765 3766 3767 3768 3769 3770 3771 3772 3773 3774 3775 3776 3777 3778 3779 3780 3781 3782 3783 3784 3785 3786 3787 3788 3789 3790 3791 3792 3793 3794 3795 3796 3797 3798 3799 3800 3801 3802 3803 3804 3805 3806 3807 3808 3809 3810 3811 3812 3813 3814 3815 3816 3817 3818 3819 3820 3821 3822 3823 3824 3825 3826 3827 3828 3829 3830 3831 3832 3833 3834 3835 3836 3837 3838 3839 3840 |
# File 'lib/parse/query.rb', line 3732 def build_aggregation_pipeline pipeline = [] # `compile_where` is already marker-free; `compile_markers` retains # the __aggregation_pipeline marker we need to extract stages from. compiled_where = compile_where markers = compile_markers has_lookup_stages = false # Collect match conditions and stages initial_match_conditions = [] aggregation_match_conditions = [] non_match_stages = [] lookup_stages = [] post_lookup_match = {} # `compiled_where` is already marker-free; use as-is. regular_constraints = compiled_where # Process regular constraints if regular_constraints.any? # Convert symbols to strings and handle date objects for MongoDB aggregation stringified_constraints = convert_dates_for_aggregation(JSON.parse(regular_constraints.to_json)) # Extract $inQuery/$notInQuery and convert to $lookup stages if has_subquery_constraints?(stringified_constraints) lookup_result = extract_subquery_to_lookup_stages(stringified_constraints) stringified_constraints = lookup_result[:constraints] lookup_stages = lookup_result[:lookup_stages] post_lookup_match = lookup_result[:post_lookup_match] has_lookup_stages = lookup_stages.any? end # Convert remaining pointer field names and values to MongoDB aggregation format if stringified_constraints.any? stringified_constraints = convert_constraints_for_aggregation(stringified_constraints) initial_match_conditions << stringified_constraints end end # Extract aggregation pipeline stages (from empty_or_nil, set_equals, etc.) if markers.key?("__aggregation_pipeline") markers["__aggregation_pipeline"].each do |stage| if stage.is_a?(Hash) && stage.key?("$match") # Aggregation $match conditions go after lookup aggregation_match_conditions << stage["$match"] else # Non-$match stages go directly to pipeline non_match_stages << stage end end end # Stage 1: Initial $match with regular constraints (before lookup) # This filters down the dataset before the expensive $lookup if initial_match_conditions.any? if initial_match_conditions.length == 1 pipeline << { "$match" => initial_match_conditions.first } else pipeline << { "$match" => { "$and" => initial_match_conditions } } end end # Stage 2: $lookup stages for subqueries ($addFields, $lookup) # These join with related collections and filter based on subquery conditions if lookup_stages.any? # Add $addFields and $lookup stages (skip $project stages) lookup_stages.each do |stage| next if stage.key?("$project") pipeline << stage end # Stage 3: Post-lookup $match to filter based on lookup results if post_lookup_match.any? pipeline << { "$match" => post_lookup_match } end # Note: We intentionally skip cleanup $project stage because: # 1. Parse Server's aggregation result processing ignores unknown fields # 2. Using $project with exclusions can cause issues in some MongoDB versions # 3. The temporary lookup fields (_lookup_*_id, _lookup_*_result) won't affect the output end # Stage 5: Aggregation $match conditions (from empty_or_nil, set_equals, etc.) if aggregation_match_conditions.any? if aggregation_match_conditions.length == 1 pipeline << { "$match" => aggregation_match_conditions.first } else pipeline << { "$match" => { "$and" => aggregation_match_conditions } } end end # Stage 6: Non-$match stages from aggregation pipeline pipeline.concat(non_match_stages) # Stage 7: Add limit if specified if @limit.is_a?(Numeric) && @limit > 0 pipeline << { "$limit" => @limit } end # Stage 8: Add skip if specified if @skip > 0 pipeline << { "$skip" => @skip } end # Optimize pipeline by merging consecutive $match stages pipeline = deduplicate_consecutive_match_stages(pipeline) [pipeline, has_lookup_stages] end |
#build_direct_mongodb_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).
2594 2595 2596 2597 2598 2599 2600 2601 2602 2603 2604 2605 2606 2607 2608 2609 2610 2611 2612 2613 2614 2615 2616 2617 2618 2619 2620 2621 2622 2623 2624 2625 2626 2627 2628 2629 2630 2631 2632 2633 2634 2635 2636 2637 2638 2639 2640 2641 2642 2643 2644 2645 2646 2647 2648 2649 2650 2651 2652 2653 2654 2655 2656 2657 2658 2659 2660 2661 2662 2663 2664 2665 2666 2667 2668 2669 2670 2671 2672 2673 2674 2675 2676 2677 2678 2679 2680 2681 2682 2683 2684 |
# File 'lib/parse/query.rb', line 2594 def build_direct_mongodb_pipeline pipeline = [] # Mirror the REST compile() behavior: ensure each top-level included field # is also in @keys so the $project stage below does not strip the pointer # that the $lookup stage is supposed to expand. merge_includes_into_keys! # Compile the where clause and convert for direct MongoDB access. # `compile_where` already strips `__`-prefixed routing markers; use # `compile_markers` to recover the unfiltered hash for the # __aggregation_pipeline extraction below. compiled_where = compile_where markers = compile_markers # Note: the `_rperm` injection for scope_to_user no longer # happens here. It moved to Parse::MongoDB.aggregate via the # acl_user: kwarg so the same three-layer ACL simulation # (top-level $match + $lookup rewriter + post-fetch redactor) # runs for scope_to_user, session_token, and the public-only # fallback alike. See {#mongo_direct_auth_kwargs}. if compiled_where.present? # Convert field names and values for direct MongoDB access. # `compiled_where` is already marker-free, so no further # reject pass is required. mongo_constraints = convert_constraints_for_direct_mongodb(compiled_where) pipeline << { "$match" => mongo_constraints } if mongo_constraints.any? end # Handle aggregation pipeline stages (from empty_or_nil, set_equals, etc.) if markers.key?("__aggregation_pipeline") markers["__aggregation_pipeline"].each do |stage| pipeline << convert_stage_for_direct_mongodb(stage) end end # Add sort stage if order is specified if @order.any? sort_spec = {} @order.each do |order_clause| # Handle both Parse::Order objects and string representations if order_clause.is_a?(Parse::Order) field = order_clause.field.to_s direction = order_clause.direction == :desc ? -1 : 1 sort_spec[convert_field_for_direct_mongodb(field)] = direction elsif order_clause.is_a?(String) # Parse order clause (e.g., "-createdAt" or "name") if order_clause.start_with?("-") field = order_clause[1..-1] sort_spec[convert_field_for_direct_mongodb(field)] = -1 else sort_spec[convert_field_for_direct_mongodb(order_clause)] = 1 end end end pipeline << { "$sort" => sort_spec } if sort_spec.any? end # Add include/eager loading $lookup stages if @includes is populated # These stages resolve pointer fields to full objects if @includes.any? include_stages = build_include_lookup_stages(@includes) pipeline.concat(include_stages) end # Add skip stage if specified pipeline << { "$skip" => @skip } if @skip > 0 # Add limit stage if specified pipeline << { "$limit" => @limit } if @limit.is_a?(Numeric) && @limit > 0 # Add $project stage if specific keys are requested # Always include required fields: _id, _created_at, _updated_at, _acl if @keys.any? project_stage = { "_id" => 1, "_created_at" => 1, "_updated_at" => 1, "_acl" => 1, } @keys.each do |key| mongo_field = convert_field_for_direct_mongodb(key.to_s) project_stage[mongo_field] = 1 end pipeline << { "$project" => project_stage } end # Optimize pipeline by merging consecutive $match stages deduplicate_consecutive_match_stages(pipeline) end |
#build_filter_condition(where) ⇒ Hash
Build a $filter condition expression from where constraints
3941 3942 3943 3944 3945 3946 3947 3948 3949 3950 3951 3952 3953 3954 3955 3956 3957 3958 3959 |
# File 'lib/parse/query.rb', line 3941 def build_filter_condition(where) conditions = where.map do |field, value| if value.is_a?(Hash) # Handle operators like $gt, $lt, etc. value.map do |op, val| { op => ["$$item.#{field}", val] } end else # Simple equality { "$eq" => ["$$item.#{field}", value] } end end.flatten if conditions.length == 1 conditions.first else { "$and" => conditions } end end |
#build_include_lookup_stages(includes) ⇒ Array<Hash>
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Build $lookup stages for included pointer fields in direct MongoDB queries. This enables eager loading of related objects when using results_direct.
2692 2693 2694 2695 2696 2697 2698 2699 2700 2701 2702 2703 2704 2705 2706 2707 2708 2709 2710 2711 2712 2713 2714 2715 2716 2717 2718 2719 2720 2721 2722 2723 2724 2725 2726 2727 2728 2729 2730 2731 2732 2733 2734 2735 2736 2737 2738 2739 2740 2741 2742 2743 2744 2745 2746 2747 2748 |
# File 'lib/parse/query.rb', line 2692 def build_include_lookup_stages(includes) return [] if includes.nil? || includes.empty? stages = [] includes.each do |field| # Handle nested includes (e.g., 'artist.label') - only process first level field_str = field.to_s base_field = field_str.split(".").first.to_sym # Get target class from model references target_class = get_pointer_target_class(base_field) next unless target_class # MongoDB pointer field name mongo_pointer_field = "_p_#{base_field}" lookup_result_field = "_included_#{base_field}" lookup_id_field = "_include_id_#{base_field}" # Stage 1: Extract objectId from pointer string using $split # Parse pointers are stored as "ClassName$objectId" stages << { "$addFields" => { lookup_id_field => { "$arrayElemAt" => [ { "$split" => ["$#{mongo_pointer_field}", { "$literal" => "$" }] }, 1, ], }, }, } # Stage 2: $lookup to join with target collection stages << { "$lookup" => { "from" => target_class, "localField" => lookup_id_field, "foreignField" => "_id", "as" => lookup_result_field, }, } # Stage 3: Unwind the array (since $lookup returns array, but we want single object) stages << { "$unwind" => { "path" => "$#{lookup_result_field}", "preserveNullAndEmptyArrays" => true, }, } # Stage 4: Clean up temporary lookup ID field stages << { "$unset" => lookup_id_field, } end stages end |
#clause(clause_name = :where) ⇒ Object
returns the query clause for the particular clause
601 602 603 604 |
# File 'lib/parse/query.rb', line 601 def clause(clause_name = :where) return unless [:keys, :where, :order, :includes, :limit, :skip].include?(clause_name) instance_variable_get "@#{clause_name}".to_sym end |
#clear(item = :results) ⇒ self
Clear a specific clause of this query. This can be one of: :where, :order, :includes, :skip, :limit, :count, :keys or :results.
447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 |
# File 'lib/parse/query.rb', line 447 def clear(item = :results) case item when :where # an array of Parse::Constraint subclasses @where = [] when :order # an array of Parse::Order objects @order = [] when :includes @includes = [] when :skip @skip = 0 when :limit @limit = nil when :count @count = 0 when :keys @keys = [] end @results = nil self # chaining end |
#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
5427 5428 5429 5430 5431 5432 5433 5434 5435 5436 5437 5438 5439 5440 5441 5442 5443 5444 5445 5446 5447 5448 5449 5450 5451 |
# File 'lib/parse/query.rb', line 5427 def clone cloned_query = Parse::Query.new(self.instance_variable_get(:@table)) # Note: :client is intentionally excluded - it contains non-serializable objects # (Redis connections, Faraday connections) and should be obtained lazily [:count, :where, :order, :keys, :exclude_keys, :includes, :limit, :skip, :cache, :use_master_key, :hint].each do |param| if instance_variable_defined?(:"@#{param}") value = instance_variable_get(:"@#{param}") if value.is_a?(Array) || value.is_a?(Hash) # Use Marshal for deep copy of complex constraint objects begin cloned_value = Marshal.load(Marshal.dump(value)) rescue => e # Fallback to shallow copy if Marshal fails puts "[Parse::Query.clone] Marshal failed for #{param}: #{e.}, 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.
4133 4134 4135 4136 4137 4138 4139 4140 4141 4142 4143 4144 4145 4146 4147 4148 4149 4150 4151 4152 4153 4154 4155 4156 4157 4158 4159 4160 4161 4162 4163 4164 4165 4166 4167 4168 4169 4170 4171 4172 4173 4174 4175 4176 4177 4178 |
# File 'lib/parse/query.rb', line 4133 def compile(encode: true, includeClassName: false) # Validate includes vs keys before compiling validate_includes_vs_keys # When a `keys` allowlist is set alongside `include`, the parent pointer # field must also be in `keys` or Parse Server strips it before expanding # the include. Auto-add the top-level segment of each include so partial # fetches don't silently drop included pointers. merge_includes_into_keys! run_callbacks :prepare do q = {} #query q[:limit] = @limit if @limit.is_a?(Numeric) && @limit > 0 q[:skip] = @skip if @skip > 0 q[:include] = @includes.join(",") unless @includes.empty? q[:keys] = @keys.join(",") unless @keys.empty? q[:excludeKeys] = @exclude_keys.join(",") if encode && @exclude_keys&.any? q[:order] = @order.join(",") unless @order.empty? unless @where.empty? q[:where] = Parse::Query.compile_where(@where) q[:where] = q[:where].to_json if encode end if @count && @count > 0 # if count is requested q[:limit] = 0 q[:count] = 1 end # Read preference must ride the REST query body (restOptions), NOT a # header: Parse Server's middleware does not map any # `X-Parse-Read-Preference` header into request options, so the # header alone is silently ignored and the read always hits the # primary. `RestQuery` reads `readPreference` from restOptions, so # emitting it here is what actually routes the read. (The header is # still sent for any intermediary that honors it; it is harmless.) if encode && (pref = normalized_read_preference) q[:readPreference] = pref end q[:hint] = @hint if @hint if includeClassName q[:className] = @table end q end end |
#compile_where ⇒ Hash
Returns a hash representing just the where clause of this
query, with SDK-internal routing markers stripped.
4182 4183 4184 |
# File 'lib/parse/query.rb', line 4182 def compile_where self.class.compile_where(@where || []) end |
#conditions(expressions = {}) ⇒ self Also known as: query, append
Add a set of query expressions and constraints.
522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 |
# File 'lib/parse/query.rb', line 522 def conditions(expressions = {}) expressions.each do |expression, value| # Normalize to symbol for comparison (handles both string and symbol keys) expr_sym = expression.respond_to?(:to_sym) ? expression.to_sym : expression if expr_sym == :order order value elsif expr_sym == :keys keys value elsif expr_sym == :key keys [value] elsif expr_sym == :skip skip value elsif expr_sym == :limit limit value elsif expr_sym == :include || expr_sym == :includes includes(value) elsif expr_sym == :cache self.cache = value elsif expr_sym == :use_master_key self.use_master_key = value elsif expr_sym == :session # you can pass a session token or a Parse::Session self.session_token = value elsif expr_sym == :read_preference self.read_preference = value # ACL convenience query options elsif expr_sym == :readable_by readable_by(value) elsif expr_sym == :writable_by writable_by(value) elsif expr_sym == :readable_by_role readable_by_role(value) elsif expr_sym == :writable_by_role writable_by_role(value) elsif expr_sym == :publicly_readable publicly_readable if value elsif expr_sym == :publicly_writable publicly_writable if value elsif expr_sym == :privately_readable || expr_sym == :master_key_read_only privately_readable if value elsif expr_sym == :privately_writable || expr_sym == :master_key_write_only privately_writable if value elsif expr_sym == :private_acl || expr_sym == :master_key_only private_acl if value elsif expr_sym == :not_publicly_readable not_publicly_readable if value elsif expr_sym == :not_publicly_writable not_publicly_writable if value else add_constraint(expression, value) end end # each self #chaining end |
#constraints(raw = false) ⇒ Array<Parse::Constraint>, Hash
1011 1012 1013 |
# File 'lib/parse/query.rb', line 1011 def constraints(raw = false) raw ? where_constraints : @where end |
#convert_addfields_for_direct_mongodb(spec) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Convert a $addFields / $set stage for direct MongoDB. Same shape
as $project: { aliasName => <expression> }. Output aliases pass
through verbatim; each value is walked as an aggregation
expression so storage-column references inside reach the correct
column via the schema-aware #convert_field_for_direct_mongodb.
3094 3095 3096 3097 3098 3099 3100 3101 3102 |
# File 'lib/parse/query.rb', line 3094 def convert_addfields_for_direct_mongodb(spec) return spec unless spec.is_a?(Hash) result = {} spec.each do |field, value| result[field] = rewrite_expression_for_direct_mongodb(value) end result end |
#convert_constraints_for_direct_mongodb(constraints) ⇒ Hash
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Convert constraints for direct MongoDB execution.
2786 2787 2788 2789 2790 2791 2792 2793 2794 2795 2796 2797 2798 2799 2800 2801 2802 2803 2804 2805 2806 2807 2808 2809 2810 2811 2812 2813 2814 2815 2816 2817 2818 2819 2820 2821 2822 2823 2824 2825 2826 2827 2828 2829 2830 2831 2832 |
# File 'lib/parse/query.rb', line 2786 def convert_constraints_for_direct_mongodb(constraints) return constraints unless constraints.is_a?(Hash) # $relatedTo resolves a Parse Relation, which is stored in the # `_Join:<key>:<ParentClass>` collection — a join the SDK does NOT # translate on the mongo-direct path. Passed through verbatim it reaches # MongoDB as an unknown `$match` operator and fails with an opaque error; # and any future attempt to rewrite it into a `$lookup` would have to # re-implement the `_rperm` / protectedFields enforcement that the rest of # this path applies post-fetch. Parse Server's own `$relatedTo` was found # to bypass exactly that enforcement (GHSA-wmwx-jr2p-4j4r), so fail closed # here with a clear message rather than risk a silent leak: this query # must run via REST (the default), where Parse Server resolves the # relation under its own ACL / CLP enforcement. if constraints.key?("$relatedTo") || constraints.key?(:"$relatedTo") raise ArgumentError, "[Parse::Query] $relatedTo cannot run on the mongo-direct path; a " \ "Parse Relation is resolved server-side via its join collection. Run " \ "this query via REST (omit `mongo_direct:` / `.results_direct` and any " \ "direct-only constraint), or express the membership as an `$inQuery` " \ "against the relation's join collection." end result = {} constraints.each do |field, value| field_str = field.to_s # Skip special operators if field_str.start_with?("$") # Recursively convert nested constraints in $and, $or, $nor if value.is_a?(Array) && %w[$and $or $nor].include?(field_str) result[field_str] = value.map { |v| convert_constraints_for_direct_mongodb(v) } else result[field_str] = value end next end # Convert field name for MongoDB mongo_field = convert_field_for_direct_mongodb(field_str) # Convert value result[mongo_field] = convert_value_for_direct_mongodb(field_str, value) end result end |
#convert_field_for_direct_mongodb(field) ⇒ String
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Convert a field name for direct MongoDB access.
2838 2839 2840 2841 2842 2843 2844 2845 2846 2847 2848 2849 2850 2851 2852 2853 2854 2855 2856 2857 2858 2859 2860 2861 2862 2863 2864 2865 2866 2867 2868 2869 2870 2871 2872 2873 2874 2875 2876 2877 2878 2879 2880 2881 2882 2883 2884 2885 2886 2887 2888 2889 2890 2891 2892 2893 2894 2895 2896 2897 2898 2899 2900 2901 2902 2903 2904 2905 2906 2907 2908 2909 2910 2911 2912 2913 2914 2915 2916 2917 2918 2919 2920 2921 2922 |
# File 'lib/parse/query.rb', line 2838 def convert_field_for_direct_mongodb(field) field_str = field.to_s # Any field name starting with underscore is non-user-facing and is # passed through verbatim. Parse user-facing properties never start # with `_` (the SDK columnizes snake_case to camelCase before save, # and Parse Server reserves the leading-underscore namespace), so a # field that does is one of: # - a MongoDB/Parse Server internal column (`_id`, `_created_at`, # `_acl`, `_rperm`, `_wperm`, `_hashed_password`, # `_session_token`, `_email_verify_token`, ...) # - a Parse-on-Mongo pointer storage column (`_p_<field>`) # - an SDK-built pipeline-temp alias such as the # `_lookup_<field>_result` / `_lookup_<field>_id` aliases that # `extract_subquery_to_lookup_stages` introduces when an # `$inQuery` constraint compiles to a `$lookup` stage # Columnizing any of these would corrupt the reference: the # previous behavior of routing `_lookup_project_result` through # `format_field` produced `lookupProjectResult` (leading underscore # stripped, snake_case to camelCase), and the post-lookup # `$match` then asked MongoDB for a column that didn't exist, so # every document silently satisfied the constraint. return field_str if field_str.start_with?("_") # Apply field formatting for regular fields formatted = Query.format_field(field) case formatted when "objectId" "_id" when "createdAt" "_created_at" when "updatedAt" "_updated_at" else # Schema-aware passthrough: only rewrite names that correspond # to a declared Parse property (or the universal built-ins # handled above). Anything else is treated as a pipeline-local # alias — `$group` accumulator name, `$project` computed field, # `$addFields` output — and the literal text passes through so # the reference matches the output key the upstream stage # produced. # # Concretely: `$status` on a class that declares `status` # remains `status` (`format_field` is a no-op for already- # camelCase names); `$author` on a class that declares a # pointer `author` becomes `$_p_author`; `$contributor_set` # (an alias the caller introduced in `$group`) stays # `$contributor_set` because no such property exists in the # schema. Callers reading the result row by `row[alias_name]` # see exactly the spelling they wrote into the pipeline. # # @note Two documented limitations of the schema-aware rule: # # 1. **Alias shadowing.** An alias whose name shadows a # declared Parse property (`$group { author: ... }` where # `author` is a pointer) is treated as the property — # downstream `$author` references resolve to `$_p_author`, # the storage column, not the alias. Avoid alias names that # collide with declared property names. The same naming # constraint MongoDB aggregation pipelines have generally; # not unique to parse-stack. # # 2. **Undeclared server columns.** Conversely, a `$field` # reference whose name corresponds to a column that exists # on the server but is NOT declared as a property on the # Ruby model passes through verbatim. The schema we consult # is the SDK-side property registry; we do not introspect # the live server schema on every translation. If you need # references in mongo-direct pipelines to translate # snake_case → camelCase or take a `_p_*` prefix, declare # the corresponding property on the Ruby model. Workaround # without declaring: write the storage-column name directly # (`$_p_author`, `$companyName`), which short-circuits the # walker via the leading-underscore / already-formatted # paths. return field_str unless field_is_known_to_schema?(formatted) if field_is_pointer?(formatted) "_p_#{formatted}" else formatted end end end |
#convert_group_for_direct_mongodb(group) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Convert $group stage for direct MongoDB. Output-alias keys
(_id, accumulator names like contributor_set) pass through
verbatim so the result row uses whatever spelling the caller
wrote. Each value — the _id group-key expression and every
accumulator expression — is walked as an aggregation expression
so $field references inside reach the correct storage column
(_p_* for pointers, _id/_created_at/_updated_at for
built-ins, untouched for unknown names i.e. pipeline-local
aliases) via the schema-aware
#convert_field_for_direct_mongodb.
3078 3079 3080 3081 3082 3083 3084 3085 3086 |
# File 'lib/parse/query.rb', line 3078 def convert_group_for_direct_mongodb(group) return group unless group.is_a?(Hash) result = {} group.each do |field, value| result[field] = rewrite_expression_for_direct_mongodb(value) end result end |
#convert_match_for_direct_mongodb(match) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Convert a $match stage for direct MongoDB. Rewrites top-level field-name keys via #convert_constraints_for_direct_mongodb and additionally walks the value of a top-level $expr as an aggregation expression so nested $fieldName references are rewritten.
3022 3023 3024 3025 3026 3027 3028 3029 3030 3031 3032 3033 3034 3035 |
# File 'lib/parse/query.rb', line 3022 def convert_match_for_direct_mongodb(match) converted = convert_constraints_for_direct_mongodb(match) return converted unless converted.is_a?(Hash) # The constraint converter passes $expr through unchanged. Rewrite # its value here so e.g. {$expr: {$eq: ["$author", "$approver"]}} # becomes {$expr: {$eq: ["$_p_author", "$_p_approver"]}}. expr_key = converted.key?("$expr") ? "$expr" : (converted.key?(:"$expr") ? :"$expr" : nil) return converted unless expr_key result = converted.dup result[expr_key] = rewrite_expression_for_direct_mongodb(converted[expr_key]) result end |
#convert_projection_for_direct_mongodb(projection) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Convert projection fields for direct MongoDB. Output-key aliases
pass through verbatim — what the caller writes is what the result
row will be keyed by. Values that are aggregation expressions
(e.g. { "$cond": [...] }) are walked recursively so nested
$fieldName references reach the correct storage column via the
schema-aware rewriter in #convert_field_for_direct_mongodb.
3044 3045 3046 3047 3048 3049 3050 3051 3052 |
# File 'lib/parse/query.rb', line 3044 def convert_projection_for_direct_mongodb(projection) return projection unless projection.is_a?(Hash) result = {} projection.each do |field, value| result[field] = rewrite_expression_for_direct_mongodb(value) end result end |
#convert_replace_root_for_direct_mongodb(spec) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Convert a $replaceRoot stage for direct MongoDB. Argument shape is
{ newRoot: <expression> }; only the newRoot value is an
expression. (Use #rewrite_expression_for_direct_mongodb directly
for $replaceWith, whose argument is the expression itself.)
3109 3110 3111 3112 3113 3114 3115 3116 3117 3118 |
# File 'lib/parse/query.rb', line 3109 def convert_replace_root_for_direct_mongodb(spec) return rewrite_expression_for_direct_mongodb(spec) unless spec.is_a?(Hash) new_root_key = spec.key?("newRoot") ? "newRoot" : (spec.key?(:newRoot) ? :newRoot : nil) return rewrite_expression_for_direct_mongodb(spec) unless new_root_key result = spec.dup result[new_root_key] = rewrite_expression_for_direct_mongodb(spec[new_root_key]) result end |
#convert_sort_for_direct_mongodb(sort) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Convert sort specification for direct MongoDB.
3056 3057 3058 3059 3060 3061 3062 3063 3064 3065 |
# File 'lib/parse/query.rb', line 3056 def convert_sort_for_direct_mongodb(sort) return sort unless sort.is_a?(Hash) result = {} sort.each do |field, direction| mongo_field = convert_field_for_direct_mongodb(field) result[mongo_field] = direction end result end |
#convert_stage_for_direct_mongodb(stage) ⇒ Hash
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Convert an aggregation stage for direct MongoDB execution.
Projection-shape stages ($project, $addFields, $set, $replaceRoot, $replaceWith) and accumulator/grouping stages ($group) carry aggregation expressions that can reference fields via $fieldName strings. These references must be rewritten to the direct-MongoDB column form (camelCase, p* for pointers, _id/_created_at/_updated_at for built-ins). The rewrite walks recursively into $cond / $eq / $switch / $expr argument arrays so a nested reference is not missed. See #rewrite_expression_for_direct_mongodb.
$match is special: its top-level keys are field-name constraints (rewritten via the constraint converter), but the value of a top-level $expr is an aggregation expression that must also be walked.
2989 2990 2991 2992 2993 2994 2995 2996 2997 2998 2999 3000 3001 3002 3003 3004 3005 3006 3007 3008 3009 3010 3011 3012 3013 3014 3015 |
# File 'lib/parse/query.rb', line 2989 def convert_stage_for_direct_mongodb(stage) return stage unless stage.is_a?(Hash) result = {} stage.each do |operator, value| case operator.to_s when "$match" result[operator] = convert_match_for_direct_mongodb(value) when "$project" result[operator] = convert_projection_for_direct_mongodb(value) when "$sort" result[operator] = convert_sort_for_direct_mongodb(value) when "$group" result[operator] = convert_group_for_direct_mongodb(value) when "$addFields", "$set" result[operator] = convert_addfields_for_direct_mongodb(value) when "$replaceRoot" result[operator] = convert_replace_root_for_direct_mongodb(value) when "$replaceWith" # $replaceWith's argument is the new-root expression directly. result[operator] = rewrite_expression_for_direct_mongodb(value) else result[operator] = value end end result end |
#convert_value_for_direct_mongodb(field, value) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Convert a value for direct MongoDB execution.
2929 2930 2931 2932 2933 2934 2935 2936 2937 2938 2939 2940 2941 2942 2943 2944 2945 2946 2947 2948 2949 2950 2951 2952 2953 2954 2955 2956 2957 2958 2959 2960 2961 2962 2963 2964 2965 2966 2967 2968 2969 2970 |
# File 'lib/parse/query.rb', line 2929 def convert_value_for_direct_mongodb(field, value) case value when Hash # Handle both string and symbol keys for __type checks type_value = value["__type"] || value[:__type] if type_value == "Pointer" # Convert Parse pointer to MongoDB pointer string format class_name = value["className"] || value[:className] object_id = value["objectId"] || value[:objectId] "#{class_name}$#{object_id}" elsif type_value == "Date" # Convert Parse Date format to Time object for BSON Date iso_value = value["iso"] || value[:iso] Time.parse(iso_value).utc else # Recursively convert nested hash (for operators like $gt, $in, etc.) # Convert symbol keys to strings for MongoDB converted = {} value.each do |k, v| key_str = k.to_s converted[key_str] = convert_value_for_direct_mongodb(field, v) end converted end when Parse::Pointer "#{value.parse_class}$#{value.id}" when Parse::Date # Parse::Date extends DateTime - convert to Time for BSON Date value.to_time.utc when Time value.utc when DateTime value.to_time.utc when Date value.to_time.utc when Array value.map { |v| convert_value_for_direct_mongodb(field, v) } else value end end |
#count(mongo_direct: false) ⇒ Integer
Perform a count query.
1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 |
# File 'lib/parse/query.rb', line 1238 def count(mongo_direct: false) # Use direct MongoDB query if requested return count_direct if mongo_direct # Auto-route to mongo-direct when the compiled where contains a # direct-only constraint. Same gate as #results. if requires_mongo_direct? assert_mongo_direct_routable! return count_direct(**mongo_direct_auth_kwargs) end # Check if this query requires aggregation pipeline processing if requires_aggregation_pipeline? # Build aggregation pipeline with $count stage pipeline, has_lookup_stages = build_aggregation_pipeline pipeline << { "$count" => "count" } # Auto-detect if MongoDB direct is needed use_mongo_direct = false if has_lookup_stages && defined?(Parse::MongoDB) && Parse::MongoDB.enabled? use_mongo_direct = true end # Execute aggregation aggregation = Aggregation.new(self, pipeline, verbose: @verbose_aggregate, mongo_direct: use_mongo_direct) response = aggregation.execute! # Extract count from aggregation result if use_mongo_direct # MongoDB direct returns raw array return 0 if response.nil? || response.empty? response.first["count"] || 0 else return 0 if response.error? || !response.result.is_a?(Array) || response.result.empty? response.result.first["count"] || 0 end else # Use standard count endpoint for non-aggregation queries old_value = @count @count = 1 res = client.find_objects(@table, compile.as_json, **_opts).count @count = old_value res end end |
#count_direct(session_token: nil, master: nil, acl_user: nil, acl_role: nil) ⇒ Integer
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.
2199 2200 2201 2202 2203 2204 2205 2206 2207 2208 2209 2210 2211 2212 2213 2214 2215 2216 2217 2218 2219 2220 2221 2222 2223 2224 2225 2226 2227 2228 2229 2230 2231 2232 2233 2234 2235 2236 2237 2238 2239 2240 2241 2242 2243 |
# File 'lib/parse/query.rb', line 2199 def count_direct(session_token: nil, master: nil, acl_user: nil, acl_role: nil) require_relative "mongodb" Parse::MongoDB.require_gem! unless Parse::MongoDB.available? raise Parse::MongoDB::NotEnabled, "Direct MongoDB queries are not enabled. " \ "Call Parse::MongoDB.configure(uri: 'mongodb://...', enabled: true) first." end # Build the aggregation pipeline for direct MongoDB execution pipeline = build_direct_mongodb_pipeline # Remove limit and skip for count (we want total count) pipeline = pipeline.reject { |stage| stage.key?("$limit") || stage.key?("$skip") } # Add count stage pipeline << { "$count" => "count" } # When no explicit auth kwargs are provided, derive them from the # query's own auth state — same fallback as results_direct. if session_token.nil? && master.nil? && acl_user.nil? && acl_role.nil? auth = mongo_direct_auth_kwargs session_token = auth[:session_token] master = auth[:master] acl_user = auth[:acl_user] acl_role = auth[:acl_role] end # SDK-built pipeline only — see results_direct for rationale. # ACL simulation runs inside Parse::MongoDB.aggregate when # session_token: or master: is supplied. raw_results = Parse::MongoDB.aggregate(@table, pipeline, allow_internal_fields: true, session_token: session_token, master: master, acl_user: acl_user, acl_role: acl_role, read_preference: @read_preference, hint: @hint) # Extract count from result return 0 if raw_results.empty? raw_results.first["count"] || 0 end |
#count_distinct(field) ⇒ Integer
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.
1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 |
# File 'lib/parse/query.rb', line 1296 def count_distinct(field) if field.nil? || !field.respond_to?(:to_s) raise ArgumentError, "Invalid field name passed to `count_distinct`." end # Format field name according to Parse conventions # Handle special MongoDB field mappings for aggregation formatted_field = case field.to_s when "created_at", "createdAt" "_created_at" when "updated_at", "updatedAt" "_updated_at" else Query.format_field(field) end # Build the aggregation pipeline pipeline = [ { "$group" => { "_id" => "$#{formatted_field}" } }, { "$count" => "distinctCount" }, ] # Use the Aggregation class to execute # The aggregate method will automatically handle where conditions aggregation = aggregate(pipeline, verbose: @verbose_aggregate) raw_results = aggregation.raw # Extract the count from the response if raw_results.is_a?(Array) && raw_results.first raw_results.first["distinctCount"] || 0 else 0 end end |
#cursor(limit: 100, order: nil) ⇒ Parse::Cursor
Create a cursor-based paginator for efficiently traversing large datasets.
Cursor-based pagination is more efficient than skip/offset pagination for large datasets because it uses the last seen objectId to fetch the next page, rather than skipping over records.
3187 3188 3189 |
# File 'lib/parse/query.rb', line 3187 def cursor(limit: 100, order: nil) Parse::Cursor.new(self, limit: limit, order: order) end |
#decode(list) ⇒ Array<Parse::Object>
Builds objects based on the set of Parse JSON hashes in an array.
3998 3999 4000 4001 4002 4003 4004 4005 4006 4007 |
# File 'lib/parse/query.rb', line 3998 def decode(list) # Pass fetched keys for partial fetch tracking (only if keys were specified) fetch_keys = @keys.present? && @keys.any? ? @keys : nil # Parse keys (not includes) to build nested fetched keys map # Keys like ["project.name", "project.status"] define which subfields to fetch on nested objects nested_keys = Parse::Query.parse_keys_to_nested_keys(@keys) if @keys.present? list.map { |m| Parse::Object.build(m, @table, fetched_keys: fetch_keys, nested_fetched_keys: nested_keys) }.compact end |
#deduplicate_consecutive_match_stages(pipeline) ⇒ Array<Hash>
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Merge consecutive $match stages in an aggregation pipeline. This optimization combines redundant stages that can occur when building pipelines from multiple constraint sources. Identical stages are deduplicated, and non-identical consecutive $match stages are merged using $and.
3291 3292 3293 3294 3295 3296 3297 3298 3299 3300 3301 3302 3303 3304 3305 3306 3307 3308 3309 3310 3311 3312 3313 3314 3315 3316 |
# File 'lib/parse/query.rb', line 3291 def deduplicate_consecutive_match_stages(pipeline) return pipeline if pipeline.empty? result = [] pipeline.each do |stage| if stage.is_a?(Hash) && stage.key?("$match") && result.last.is_a?(Hash) && result.last.key?("$match") prev_match = result.last["$match"] curr_match = stage["$match"] # Skip if identical next if prev_match == curr_match # Merge the two $match stages using $and # Handle cases where either side might already have $and prev_conditions = prev_match.key?("$and") ? prev_match["$and"] : [prev_match] curr_conditions = curr_match.key?("$and") ? curr_match["$and"] : [curr_match] # Replace the previous $match with the merged version result[-1] = { "$match" => { "$and" => prev_conditions + curr_conditions } } else result << stage end end result end |
#distinct(field, return_pointers: false, mongo_direct: false, order: nil) ⇒ Object
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.
1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 |
# File 'lib/parse/query.rb', line 1116 def distinct(field, return_pointers: false, mongo_direct: false, order: nil) # Explicit opt-in to direct MongoDB if mongo_direct return distinct_direct(field, return_pointers: return_pointers, order: order, **mongo_direct_auth_kwargs) end # Auto-route to mongo-direct when the compiled where contains a # direct-only constraint. Same gate as #count / #results. if requires_mongo_direct? assert_mongo_direct_routable! return distinct_direct(field, return_pointers: return_pointers, order: order, **mongo_direct_auth_kwargs) end # Auto-route scoped queries (session_token / acl_user / acl_role) to # mongo-direct: Parse Server's REST `/aggregate` endpoint is # master-key-only and enforces neither ACL nor CLP, so a scoped # `.distinct` call against REST would silently return unscoped # values. The mongo-direct path runs ACLScope + CLPScope before # `$group`, so distinct values reflect only ACL-readable rows. if distinct_query_is_scoped? && defined?(Parse::MongoDB) && Parse::MongoDB.enabled? return distinct_direct(field, return_pointers: return_pointers, order: order, **mongo_direct_auth_kwargs) end if field.nil? || !field.respond_to?(:to_s) || field.is_a?(Hash) || field.is_a?(Array) raise ArgumentError, "Invalid field name passed to `distinct`." end sort_dir = distinct_sort_direction(order) # Format field for aggregation formatted_field = format_aggregation_field(field) # Build the aggregation pipeline for distinct values pipeline = [{ "$group" => { "_id" => "$#{formatted_field}" } }] pipeline << { "$sort" => { "_id" => sort_dir } } if sort_dir pipeline << { "$project" => { "_id" => 0, "value" => "$_id" } } # Add match stage if there are where conditions compiled_where = compile_where if compiled_where.present? # Convert field names for aggregation context and handle dates aggregation_where = convert_constraints_for_aggregation(compiled_where) stringified_where = convert_dates_for_aggregation(aggregation_where) pipeline.unshift({ "$match" => stringified_where }) end # Use the Aggregation class to execute aggregation = aggregate(pipeline, verbose: @verbose_aggregate) raw_results = aggregation.raw # Extract values from the results values = raw_results.map { |item| item["value"] }.compact # Use schema-based approach to handle pointer field results parse_class = Parse::Model.const_get(@table) rescue nil is_pointer = parse_class && is_pointer_field?(parse_class, field, formatted_field) if is_pointer && values.any? # Convert all values using schema information converted_values = values.map do |value| convert_pointer_value_with_schema(value, field, return_pointers: return_pointers) end converted_values elsif return_pointers # Explicit conversion requested - try to convert using schema or fallback to string detection if values.any? && values.first.is_a?(String) && values.first.include?("$") to_pointers(values, field) else values.map { |value| convert_pointer_value_with_schema(value, field, return_pointers: true) } end else # Fallback to original string detection for backward compatibility if values.any? && values.first.is_a?(String) && values.first.include?("$") && values.first.match(/^[A-Za-z]\w*\$\w+$/) first_class_name = values.first.split("$", 2)[0] if values.all? { |v| v.is_a?(String) && v.start_with?("#{first_class_name}$") } values.map { |value| value.split("$", 2)[1] } else values end else values end end end |
#distinct_direct(field, return_pointers: false, order: nil, session_token: nil, master: nil, acl_user: nil, acl_role: nil) ⇒ Array
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.
2263 2264 2265 2266 2267 2268 2269 2270 2271 2272 2273 2274 2275 2276 2277 2278 2279 2280 2281 2282 2283 2284 2285 2286 2287 2288 2289 2290 2291 2292 2293 2294 2295 2296 2297 2298 2299 2300 2301 2302 2303 2304 2305 2306 2307 2308 2309 2310 2311 2312 2313 2314 2315 2316 2317 2318 2319 2320 2321 2322 2323 2324 2325 2326 2327 2328 2329 2330 2331 2332 2333 2334 2335 2336 2337 2338 2339 2340 |
# File 'lib/parse/query.rb', line 2263 def distinct_direct(field, return_pointers: false, order: nil, session_token: nil, master: nil, acl_user: nil, acl_role: nil) require_relative "mongodb" Parse::MongoDB.require_gem! unless Parse::MongoDB.available? raise Parse::MongoDB::NotEnabled, "Direct MongoDB queries are not enabled. " \ "Call Parse::MongoDB.configure(uri: 'mongodb://...', enabled: true) first." end if field.nil? || !field.respond_to?(:to_s) || field.is_a?(Hash) || field.is_a?(Array) raise ArgumentError, "Invalid field name passed to `distinct_direct`." end sort_dir = distinct_sort_direction(order) # Convert field name for direct MongoDB access mongo_field = convert_field_for_direct_mongodb(Query.format_field(field)) # Build the base pipeline with match constraints pipeline = [] # Add match stage from query constraints. `compile_where` already # strips `__`-prefixed routing markers, so the result is safe to # forward to MongoDB. compiled_where = compile_where if compiled_where.present? mongo_constraints = convert_constraints_for_direct_mongodb(compiled_where) pipeline << { "$match" => mongo_constraints } if mongo_constraints.any? end # Add group, optional sort, and project stages for distinct pipeline << { "$group" => { "_id" => "$#{mongo_field}" } } pipeline << { "$sort" => { "_id" => sort_dir } } if sort_dir pipeline << { "$project" => { "_id" => 0, "value" => "$_id" } } # SDK-built pipeline only — see results_direct for rationale. # Forward auth kwargs so Parse::MongoDB.aggregate runs the # three-layer ACL + CLP + protectedFields simulation for scoped # agents. Without this, distinct silently returns the unscoped # universe (CLP-1 enforcement asymmetry vs. #count / #results). # When no explicit auth kwargs are provided, derive from the # query's own auth state — same fallback as results_direct. if session_token.nil? && master.nil? && acl_user.nil? && acl_role.nil? auth = mongo_direct_auth_kwargs session_token = auth[:session_token] master = auth[:master] acl_user = auth[:acl_user] acl_role = auth[:acl_role] end raw_results = Parse::MongoDB.aggregate(@table, pipeline, allow_internal_fields: true, read_preference: @read_preference, hint: @hint, session_token: session_token, master: master, acl_user: acl_user, acl_role: acl_role) # Extract values from results values = raw_results.map { |doc| doc["value"] }.compact # Handle pointer conversion if needed if return_pointers || field_is_pointer?(Query.format_field(field)) values = values.map do |value| if value.is_a?(String) && value.include?("$") # MongoDB pointer format: "ClassName$objectId" class_name, object_id = value.split("$", 2) Parse::Pointer.new(class_name, object_id) else value end end end values end |
#distinct_direct_pointers(field, order: nil, session_token: nil, master: nil, acl_user: nil, acl_role: nil) ⇒ Array
Convenience method for distinct_direct that always returns Parse::Pointer objects for pointer fields.
2347 2348 2349 2350 2351 2352 |
# File 'lib/parse/query.rb', line 2347 def distinct_direct_pointers(field, order: nil, session_token: nil, master: nil, acl_user: nil, acl_role: nil) distinct_direct(field, return_pointers: true, order: order, session_token: session_token, master: master, acl_user: acl_user, acl_role: acl_role) end |
#distinct_objects(field, return_pointers: false) ⇒ Array
Enhanced distinct method that automatically populates Parse pointer objects at the server level. Uses aggregation pipeline to efficiently populate objects instead of post-processing.
4519 4520 4521 4522 4523 4524 4525 4526 |
# File 'lib/parse/query.rb', line 4519 def distinct_objects(field, return_pointers: false) if field.nil? || !field.respond_to?(:to_s) raise ArgumentError, "Invalid field name passed to `distinct_objects`." end # Use aggregation pipeline to get distinct values with populated objects execute_distinct_with_population(field, return_pointers: return_pointers) end |
#distinct_pointers(field, order: nil) ⇒ Array
Convenience method for distinct queries that always return Parse::Pointer objects for pointer fields. This is equivalent to calling distinct(field, return_pointers: true).
1209 1210 1211 |
# File 'lib/parse/query.rb', line 1209 def distinct_pointers(field, order: nil) distinct(field, return_pointers: true, order: order) end |
#distinct_query_is_scoped? ⇒ Boolean
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Whether this query carries a non-master-key auth scope. Used by
#distinct (and group_by aggregations) to decide whether to
auto-promote the REST aggregate path to mongo-direct so the SDK's
ACLScope / CLPScope enforcement actually runs.
1799 1800 1801 1802 1803 1804 |
# File 'lib/parse/query.rb', line 1799 def distinct_query_is_scoped? return true if @session_token.is_a?(String) && !@session_token.empty? return true if @acl_user return true if @acl_role false end |
#each { ... } ⇒ Array
1334 1335 1336 1337 |
# File 'lib/parse/query.rb', line 1334 def each(&block) return results.enum_for(:each) unless block_given? # Sparkling magic! results.each(&block) end |
#exclude_keys(*fields) ⇒ self
On the REST query path (+encode: true+ in #compile) this maps to
Parse Server's path-scoped +excludeKeys+. On the mongo-direct path
(explicit +.results_direct+, an auto-route, or an aggregation that
auto-promotes — e.g. an +$inQuery+ pointer constraint that rewrites to
a +$lookup+) the pipeline can only project the #keys allowlist, so
the SDK honors the denylist as a post-fetch sanitize over the returned
results instead. That mongo-direct sanitize is recursive by name: it
strips EVERY key with a matching name at any depth, so excluding a
field also removes a same-named field inside included/nested objects —
broader than the REST path's top-level/dotted scoping. Reserved
envelope fields (+objectId+, +className+, +__type+, +createdAt+,
+updatedAt+, +ACL+ and their Mongo storage-form names) are never
stripped, so object reconstruction is unaffected. The raw aggregation
accessor (aggregate(...).raw) returns unredacted documents — the
sanitize applies to the object/decoded result paths. +excludeKeys+ is a
projection convenience, not an ACL/CLP boundary, so it does not affect
access control.
Set a server-side field denylist for this query. When set, Parse Server excludes the named fields from each returned object, complementing the #keys allowlist. The two options can be combined: Parse Server first applies the #keys allowlist, then strips any field names listed in +excludeKeys+.
666 667 668 669 670 671 672 673 674 675 676 |
# File 'lib/parse/query.rb', line 666 def exclude_keys(*fields) @exclude_keys ||= [] fields.flatten.each do |field| if field.nil? == false && field.respond_to?(:to_s) @exclude_keys.push Query.format_field(field).to_sym end end @exclude_keys.uniq! @results = nil if fields.count > 0 self # chaining end |
#execute_aggregation_pipeline ⇒ Aggregation
Execute an aggregation pipeline for queries with pipeline constraints
3694 3695 3696 3697 3698 3699 3700 3701 3702 3703 3704 3705 3706 3707 3708 3709 3710 3711 3712 3713 3714 3715 3716 3717 3718 |
# File 'lib/parse/query.rb', line 3694 def execute_aggregation_pipeline pipeline, has_lookup_stages = build_aggregation_pipeline # Determine if MongoDB direct should be used: # 1. Explicit opt-in via @acl_query_mongo_direct = true # 2. Auto-detect when lookup stages use $split with $literal (to parse pointer format), # Parse Server's REST API can't handle it correctly # 3. Auto-detect when querying internal fields like _rperm or _wperm (ACL fields), # Parse Server blocks these for security - must use MongoDB direct use_mongo_direct = false # Check for explicit mongo_direct preference first if defined?(@acl_query_mongo_direct) && !@acl_query_mongo_direct.nil? use_mongo_direct = @acl_query_mongo_direct elsif defined?(Parse::MongoDB) && Parse::MongoDB.enabled? # Auto-detect based on pipeline contents if has_lookup_stages || pipeline_uses_internal_fields?(pipeline) use_mongo_direct = true end end # Create Aggregation directly to avoid double-applying constraints # The aggregate() method would redundantly add where constraints again Aggregation.new(self, pipeline, verbose: @verbose_aggregate, mongo_direct: use_mongo_direct) end |
#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.
3262 3263 3264 3265 3266 3267 3268 3269 3270 3271 3272 3273 3274 3275 3276 3277 3278 3279 3280 3281 3282 |
# File 'lib/parse/query.rb', line 3262 def explain warn_if_public_explain_restricted! compiled_query = compile compiled_query[:explain] = true response = client.find_objects(@table, compiled_query.as_json, headers: _headers, **_opts) if response.error? # Parse Server 9.0+ defaults `allowPublicExplain` to false, so a # non-master explain that worked on 8.x now returns a permission # error. Surface that as actionable guidance instead of a bare 403. if response.respond_to?(:permission_denied?) && response. puts "[ParseQuery:Explain] #{response.error} — Parse Server 9.0+ defaults " \ "`allowPublicExplain` to false; query explain now requires the master key " \ "(use_master_key: true) or `allowPublicExplain: true` in the server's " \ "databaseOptions." else puts "[ParseQuery:Explain] #{response.error}" end return {} end response.result end |
#extract_subquery_to_lookup_stages(constraints) ⇒ Hash
Extract $inQuery and $notInQuery constraints and build $lookup stages for them. This converts Parse subquery constraints into MongoDB $lookup stages that join with the related collection and filter based on the subquery conditions. Uses raw MongoDB field names (_p_field) and returns results via .raw aggregation.
3848 3849 3850 3851 3852 3853 3854 3855 3856 3857 3858 3859 3860 3861 3862 3863 3864 3865 3866 3867 3868 3869 3870 3871 3872 3873 3874 3875 3876 3877 3878 3879 3880 3881 3882 3883 3884 3885 3886 3887 3888 3889 3890 3891 3892 3893 3894 3895 3896 3897 3898 3899 3900 3901 3902 3903 3904 3905 3906 3907 3908 3909 3910 3911 3912 3913 3914 3915 3916 3917 3918 3919 3920 3921 3922 3923 3924 3925 3926 3927 3928 3929 3930 3931 3932 3933 3934 3935 3936 |
# File 'lib/parse/query.rb', line 3848 def extract_subquery_to_lookup_stages(constraints) return { constraints: constraints, lookup_stages: [], post_lookup_match: {} } unless constraints.is_a?(Hash) remaining_constraints = {} lookup_stages = [] post_lookup_match = {} constraints.each do |field, value| # Check for both string and symbol keys has_in_query = value.is_a?(Hash) && (value.key?("$inQuery") || value.key?(:"$inQuery")) has_not_in_query = value.is_a?(Hash) && (value.key?("$notInQuery") || value.key?(:"$notInQuery")) if has_in_query || has_not_in_query is_in_query = has_in_query # Get the subquery config using the correct key type in_query_key = value.key?("$inQuery") ? "$inQuery" : :"$inQuery" not_in_query_key = value.key?("$notInQuery") ? "$notInQuery" : :"$notInQuery" subquery_config = value[is_in_query ? in_query_key : not_in_query_key] # Handle both string and symbol keys in the subquery config class_name = subquery_config["className"] || subquery_config[:className] where_clause = subquery_config["where"] || subquery_config[:where] || {} # Format field name for the pointer formatted_field = Query.format_field(field) mongo_pointer_field = "_p_#{formatted_field}" lookup_result_field = "_lookup_#{formatted_field}_result" lookup_id_field = "_lookup_#{formatted_field}_id" # Stage 1: Extract objectId from the pointer field using $split # Parse Server stores pointers as _p_fieldName with format "ClassName$objectId" # Use $literal to escape the $ character in the delimiter lookup_stages << { "$addFields" => { lookup_id_field => { "$arrayElemAt" => [ { "$split" => ["$#{mongo_pointer_field}", { "$literal" => "$" }] }, 1, ], }, }, } # Stage 2: $lookup to join with the related collection # Build pipeline to match on _id and apply where conditions lookup_pipeline = [ { "$match" => { "$expr" => { "$eq" => ["$_id", "$$lookupId"] } } }, ] # Add where conditions to lookup pipeline if present if where_clause.any? converted_where = convert_dates_for_aggregation(where_clause) converted_where = convert_constraints_for_aggregation(converted_where) lookup_pipeline << { "$match" => converted_where } end lookup_stages << { "$lookup" => { "from" => class_name, "let" => { "lookupId" => "$#{lookup_id_field}" }, "pipeline" => lookup_pipeline, "as" => lookup_result_field, }, } # Match based on whether lookup returned results if is_in_query # $inQuery: keep documents where lookup found matches post_lookup_match[lookup_result_field] = { "$ne" => [] } else # $notInQuery: keep documents where lookup found no matches post_lookup_match[lookup_result_field] = { "$eq" => [] } end elsif value.is_a?(Hash) # Recursively handle nested constraints nested = extract_subquery_to_lookup_stages(value) if nested[:lookup_stages].any? lookup_stages.concat(nested[:lookup_stages]) post_lookup_match.merge!(nested[:post_lookup_match]) remaining_constraints[field] = nested[:constraints] else remaining_constraints[field] = value end else remaining_constraints[field] = value end end { constraints: remaining_constraints, lookup_stages: lookup_stages, post_lookup_match: post_lookup_match } end |
#fetch!(compiled_query) ⇒ Parse::Response Also known as: execute!
Performs the fetch request for the query.
1673 1674 1675 1676 1677 1678 1679 |
# File 'lib/parse/query.rb', line 1673 def fetch!(compiled_query) response = client.find_objects(@table, compiled_query.as_json, headers: _headers, **_opts) if response.error? puts "[ParseQuery] #{response.error}" end response end |
#first(limit = 1) ⇒ Parse::Object #first(constraints = {}) ⇒ Parse::Object
Supports all constraint options like :keys, :includes, :order, etc.
1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 |
# File 'lib/parse/query.rb', line 1370 def first(limit_or_constraints = 1, mongo_direct: false, **) # 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.
2149 2150 2151 2152 2153 2154 2155 2156 2157 2158 2159 2160 2161 2162 2163 2164 2165 2166 2167 2168 2169 2170 2171 2172 2173 2174 2175 2176 2177 2178 2179 2180 2181 2182 2183 |
# File 'lib/parse/query.rb', line 2149 def first_direct(limit_or_constraints = 1) if limit_or_constraints.is_a?(Hash) conditions(limit_or_constraints) limit_or_constraints = 1 end count = case limit_or_constraints when Numeric then limit_or_constraints.to_i when String unless limit_or_constraints =~ /\A-?\d+\z/ raise ArgumentError, "Invalid first_direct() argument #{limit_or_constraints.inspect}. " \ "Expected an Integer, a numeric String, or a Hash of constraints." end limit_or_constraints.to_i else raise ArgumentError, "Invalid first_direct() argument #{limit_or_constraints.inspect}. " \ "Expected an Integer, a numeric String, or a Hash of constraints." end count = 1 if count <= 0 # Set limit for single/few results original_limit = @limit @limit = count begin items = results_direct ensure @limit = original_limit end count == 1 ? items.first : items.first(count) end |
#get(object_id) ⇒ Parse::Object
Retrieve a single object by its objectId.
1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 |
# File 'lib/parse/query.rb', line 1457 def get(object_id) parse_class = Object.const_get(@table) if Object.const_defined?(@table) parse_class ||= Parse::Object response = client.fetch_object(@table, object_id) if response.error? raise Parse::Error.new(response.code, response.error) end Parse::Object.build(response.result, parse_class) end |
#get_pointer_target_class(field) ⇒ String?
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Get the target class name for a pointer field from model references. Uses the model's references hash which maps field names to target class names.
2756 2757 2758 2759 2760 2761 2762 2763 2764 2765 2766 2767 2768 2769 2770 2771 2772 2773 2774 2775 2776 2777 2778 2779 2780 |
# File 'lib/parse/query.rb', line 2756 def get_pointer_target_class(field) begin klass = Parse::Model.find_class(@table) return nil unless klass.respond_to?(:references) references = klass.references return nil if references.nil? || references.empty? # Check both the field name and its formatted Parse field name formatted_field = Query.format_field(field).to_sym # Try direct lookup first, then formatted field target = references[field] || references[formatted_field] # Also check field_map for aliased fields if target.nil? && klass.respond_to?(:field_map) mapped_field = klass.field_map[field] target = references[mapped_field] if mapped_field end target rescue NameError, StandardError nil end end |
#group_by(field, flatten_arrays: false, sortable: false, return_pointers: false, mongo_direct: false) ⇒ GroupBy, SortableGroupBy
Group results by a specific field and return a GroupBy object for chaining aggregations.
4331 4332 4333 4334 4335 4336 4337 4338 4339 4340 4341 |
# File 'lib/parse/query.rb', line 4331 def group_by(field, flatten_arrays: false, sortable: false, return_pointers: false, mongo_direct: false) if field.nil? || !field.respond_to?(:to_s) raise ArgumentError, "Invalid field name passed to `group_by`." end if sortable SortableGroupBy.new(self, field, flatten_arrays: flatten_arrays, return_pointers: return_pointers, mongo_direct: mongo_direct) else GroupBy.new(self, field, flatten_arrays: flatten_arrays, return_pointers: return_pointers, mongo_direct: mongo_direct) end end |
#group_by_date(field, interval, sortable: false, return_pointers: false, timezone: nil, mongo_direct: false) ⇒ GroupByDate, SortableGroupByDate
Group results by a date field at specified time intervals.
4491 4492 4493 4494 4495 4496 4497 4498 4499 4500 4501 4502 4503 4504 4505 |
# File 'lib/parse/query.rb', line 4491 def group_by_date(field, interval, sortable: false, return_pointers: false, timezone: nil, mongo_direct: false) if field.nil? || !field.respond_to?(:to_s) raise ArgumentError, "Invalid field name passed to `group_by_date`." end unless [:year, :month, :week, :day, :hour, :minute, :second].include?(interval.to_sym) raise ArgumentError, "Invalid interval. Must be one of: :year, :month, :week, :day, :hour, :minute, :second" end if sortable SortableGroupByDate.new(self, field, interval.to_sym, return_pointers: return_pointers, timezone: timezone, mongo_direct: mongo_direct) else GroupByDate.new(self, field, interval.to_sym, return_pointers: return_pointers, timezone: timezone, mongo_direct: mongo_direct) end end |
#group_objects_by(field, return_pointers: false) ⇒ Hash
Group Parse objects by a field value and return arrays of actual objects. Unlike group_by which uses aggregation for counts/sums, this fetches all objects and groups them in Ruby, returning the actual Parse object instances.
4365 4366 4367 4368 4369 4370 4371 4372 4373 4374 4375 4376 4377 4378 4379 4380 4381 4382 4383 4384 4385 4386 4387 4388 4389 4390 4391 4392 4393 4394 4395 4396 4397 4398 4399 4400 4401 4402 4403 4404 4405 4406 4407 |
# File 'lib/parse/query.rb', line 4365 def group_objects_by(field, return_pointers: false) if field.nil? || !field.respond_to?(:to_s) raise ArgumentError, "Invalid field name passed to `group_objects_by`." end # Fetch all objects that match the query objects = results(return_pointers: return_pointers) # Group objects by the specified field value grouped = {} objects.each do |obj| # Get the field value for grouping field_value = if obj.respond_to?(:attributes) # For Parse objects, try multiple field access patterns obj.attributes[field.to_s] || obj.attributes[Query.format_field(field).to_s] || (obj.respond_to?(field) ? obj.send(field) : nil) elsif obj.is_a?(Hash) # For raw JSON objects, try multiple field access patterns obj[field.to_s] || obj[Query.format_field(field).to_s] || obj[field.to_sym] || obj[Query.format_field(field).to_sym] else # Fallback - try to access as method obj.respond_to?(field) ? obj.send(field) : nil end # Handle nil field values group_key = field_value.nil? ? "null" : field_value # Convert Parse pointer values to readable format for grouping key if group_key.is_a?(Hash) && group_key["__type"] == "Pointer" group_key = "#{group_key["className"]}##{group_key["objectId"]}" end # Initialize array if this is the first object for this group grouped[group_key] ||= [] grouped[group_key] << obj end grouped end |
#has_subquery_constraints?(constraints) ⇒ Boolean
Check if constraints contain $inQuery or $notInQuery that need resolution
3964 3965 3966 3967 3968 3969 3970 3971 3972 3973 3974 3975 3976 3977 3978 |
# File 'lib/parse/query.rb', line 3964 def has_subquery_constraints?(constraints) return false unless constraints.is_a?(Hash) constraints.any? do |field, value| if value.is_a?(Hash) # Check for both string and symbol keys since constraints can come from # different sources (JSON parsing vs Ruby symbol keys) value.key?("$inQuery") || value.key?(:"$inQuery") || value.key?("$notInQuery") || value.key?(:"$notInQuery") || has_subquery_constraints?(value) else false end end end |
#include(*fields) ⇒ Object
alias for includes
911 912 913 |
# File 'lib/parse/query.rb', line 911 def include(*fields) includes(*fields) end |
#includes(*fields) ⇒ self
Set a list of Parse Pointer columns to be fetched for matching records.
You may chain multiple columns with the . operator.
898 899 900 901 902 903 904 905 906 907 908 |
# File 'lib/parse/query.rb', line 898 def includes(*fields) @includes ||= [] fields.flatten.each do |field| if field.nil? == false && field.respond_to?(:to_s) @includes.push Query.format_field(field).to_sym end end @includes.uniq! @results = nil if fields.count > 0 self # chaining end |
#keys(*fields) ⇒ self Also known as: select_fields
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.
620 621 622 623 624 625 626 627 628 629 630 |
# File 'lib/parse/query.rb', line 620 def keys(*fields) @keys ||= [] fields.flatten.each do |field| if field.nil? == false && field.respond_to?(:to_s) @keys.push Query.format_field(field).to_sym end end @keys.uniq! @results = nil if fields.count > 0 self # chaining end |
#last_updated(limit = 1, **options) ⇒ Parse::Object+
Supports all constraint options like :keys, :includes, :limit, etc.
Returns the most recently updated object(s) (ordered by updated_at descending).
1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 |
# File 'lib/parse/query.rb', line 1441 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).
1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 |
# File 'lib/parse/query.rb', line 1419 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.
816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 |
# File 'lib/parse/query.rb', line 816 def limit(count) case count when nil @limit = nil when Numeric @limit = [0, count.to_i].max when :max @limit = :max when String unless count =~ /\A-?\d+\z/ raise ArgumentError, "Invalid limit #{count.inspect}. Expected an Integer, :max, " \ "a numeric String, or nil." end @limit = [0, count.to_i].max else raise ArgumentError, "Invalid limit #{count.inspect}. Expected an Integer, :max, " \ "a numeric String, or nil." end @results = nil self #chaining end |
#map { ... } ⇒ Array
1342 1343 1344 1345 |
# File 'lib/parse/query.rb', line 1342 def map(&block) return results.enum_for(:map) unless block_given? # Sparkling magic! results.map(&block) end |
#max(field) ⇒ Object
Find the maximum value for a specific field.
4288 4289 4290 4291 4292 4293 4294 4295 4296 4297 4298 4299 4300 4301 4302 |
# File 'lib/parse/query.rb', line 4288 def max(field) if field.nil? || !field.respond_to?(:to_s) raise ArgumentError, "Invalid field name passed to `max`." end # Format field name according to Parse conventions formatted_field = format_aggregation_field(field) # Build the aggregation pipeline pipeline = [ { "$group" => { "_id" => nil, "max" => { "$max" => "$#{formatted_field}" } } }, ] execute_basic_aggregation(pipeline, "max", field, "max") end |
#min(field) ⇒ Object
Find the minimum value for a specific field.
4269 4270 4271 4272 4273 4274 4275 4276 4277 4278 4279 4280 4281 4282 4283 |
# File 'lib/parse/query.rb', line 4269 def min(field) if field.nil? || !field.respond_to?(:to_s) raise ArgumentError, "Invalid field name passed to `min`." end # Format field name according to Parse conventions formatted_field = format_aggregation_field(field) # Build the aggregation pipeline pipeline = [ { "$group" => { "_id" => nil, "min" => { "$min" => "$#{formatted_field}" } } }, ] execute_basic_aggregation(pipeline, "min", field, "min") end |
#not_publicly_readable(mongo_direct: nil) ⇒ Parse::Query
Find objects that are NOT publicly readable. Matches objects where _rperm does NOT contain "*".
5609 5610 5611 5612 5613 |
# File 'lib/parse/query.rb', line 5609 def not_publicly_readable(mongo_direct: nil) @acl_query_mongo_direct = mongo_direct unless mongo_direct.nil? where(:ACL.not_readable_by => "*") self end |
#not_publicly_writable(mongo_direct: nil) ⇒ Parse::Query
Find objects that are NOT publicly writable. Matches objects where _wperm does NOT contain "*".
5622 5623 5624 5625 5626 |
# File 'lib/parse/query.rb', line 5622 def not_publicly_writable(mongo_direct: nil) @acl_query_mongo_direct = mongo_direct unless mongo_direct.nil? where(:ACL.not_writable_by => "*") self end |
#or_where(where_clauses = []) ⇒ Query
Combine two where clauses into an OR constraint. Equivalent to the $or
Parse query operation. This is useful if you want to find objects that
match several queries. We overload the | operator in order to have a
clean syntax for joining these or operations.
1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 |
# File 'lib/parse/query.rb', line 1061 def or_where(where_clauses = []) where_clauses = where_clauses.where if where_clauses.is_a?(Parse::Query) where_clauses = Parse::Query.new(@table, where_clauses).where if where_clauses.is_a?(Hash) return self if where_clauses.blank? # we can only have one compound query constraint. If we need to add another OR clause # let's find the one we have (if any) compound = @where.find { |f| f.is_a?(Parse::Constraint::CompoundQueryConstraint) } # create a set of clauses that are not an OR clause. remaining_clauses = @where.select { |f| f.is_a?(Parse::Constraint::CompoundQueryConstraint) == false } # if we don't have a OR clause to reuse, then create a new one with then # current set of constraints if compound.blank? initial_constraints = Parse::Query.compile_where(remaining_clauses) # Only include initial constraints if they're not empty initial_values = initial_constraints.empty? ? [] : [initial_constraints] compound = Parse::Constraint::CompoundQueryConstraint.new :or, initial_values end # then take the where clauses from the second query and append them. new_constraints = Parse::Query.compile_where(where_clauses) # Only add new constraints if they're not empty unless new_constraints.empty? compound.value.push new_constraints end #compound = Parse::Constraint::CompoundQueryConstraint.new :or, [remaining_clauses, or_where_query.where] @where = [compound] self #chaining end |
#order(*ordering) ⇒ self
Add a sorting order for the query.
737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 |
# File 'lib/parse/query.rb', line 737 def order(*ordering) @order ||= [] # Don't flatten through Hashes — flatten only unpacks Arrays. ordering.flatten.each do |entry| case entry when Order entry.field = Query.format_field(entry.field) @order.push entry when Symbol, String o = Order.new(entry) o.field = Query.format_field(o.field) @order.push o when Hash entry.each do |field, direction| dir_sym = direction.is_a?(String) ? direction.downcase.to_sym : direction unless dir_sym == :asc || dir_sym == :desc raise ArgumentError, "Invalid order direction #{direction.inspect} for field " \ "#{field.inspect}. Expected :asc or :desc." end o = Order.new(field, dir_sym) o.field = Query.format_field(o.field) @order.push o end else raise ArgumentError, "Invalid order argument #{entry.inspect}. Expected a Symbol, " \ "String, Parse::Order (e.g. :field.asc / :field.desc), or " \ "Hash of {field => :asc | :desc}." end end @results = nil if ordering.count > 0 self #chaining end |
#pipeline ⇒ Array
Returns the aggregation pipeline for this query if it contains pipeline-based constraints
4198 4199 4200 4201 4202 4203 4204 4205 4206 4207 4208 4209 4210 4211 4212 |
# File 'lib/parse/query.rb', line 4198 def pipeline pipeline_stages = [] # Check if any constraints generate aggregation pipelines @where.each do |constraint| if constraint.respond_to?(:as_json) constraint_json = constraint.as_json if constraint_json.is_a?(Hash) && constraint_json.has_key?("__aggregation_pipeline") pipeline_stages.concat(constraint_json["__aggregation_pipeline"]) end end end pipeline_stages end |
#pipeline_uses_internal_fields?(pipeline) ⇒ Boolean
Check if the pipeline references internal Parse fields that require MongoDB direct access
3723 3724 3725 3726 3727 |
# File 'lib/parse/query.rb', line 3723 def pipeline_uses_internal_fields?(pipeline) internal_fields = %w[_rperm _wperm _acl] pipeline_json = pipeline.to_json internal_fields.any? { |field| pipeline_json.include?(field) } end |
#pluck(field) ⇒ Array
Extract values for a specific field from all matching objects. This is similar to keys() but returns an array of the actual field values instead of objects with only those fields selected.
695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 |
# File 'lib/parse/query.rb', line 695 def pluck(field) if field.nil? || !field.respond_to?(:to_s) raise ArgumentError, "Invalid field name passed to `pluck`." end # Use keys to select only the field we want for efficiency query_with_field = self.dup.keys(field) # Get the results and extract the field values objects = query_with_field.results formatted_field = Query.format_field(field) objects.map do |obj| if obj.respond_to?(:attributes) # For Parse objects, get the attribute value obj.attributes[field.to_s] || obj.attributes[formatted_field.to_s] elsif obj.is_a?(Hash) # For raw JSON objects obj[field.to_s] || obj[formatted_field.to_s] else # Fallback - try to access as method obj.respond_to?(field) ? obj.send(field) : nil end end end |
#prepared(includeClassName: false) ⇒ Hash
Returns a compiled query without encoding the where clause.
4123 4124 4125 |
# File 'lib/parse/query.rb', line 4123 def prepared(includeClassName: false) compile(encode: false, includeClassName: includeClassName) end |
#pretty ⇒ String
Retruns a formatted JSON string representing the query, useful for debugging.
4222 4223 4224 |
# File 'lib/parse/query.rb', line 4222 def pretty JSON.pretty_generate(as_json) end |
#private_acl(mongo_direct: nil) ⇒ Parse::Query Also known as: master_key_only
Find objects with completely private ACL (no read AND no write permissions). Only accessible with master key.
5595 5596 5597 5598 |
# File 'lib/parse/query.rb', line 5595 def private_acl(mongo_direct: nil) privately_readable(mongo_direct: mongo_direct) privately_writable(mongo_direct: mongo_direct) end |
#privately_readable(mongo_direct: nil) ⇒ Parse::Query Also known as: master_key_read_only
Find objects with no read permissions (master key only). Matches objects where _rperm is empty or doesn't exist.
5567 5568 5569 |
# File 'lib/parse/query.rb', line 5567 def privately_readable(mongo_direct: nil) readable_by("none", mongo_direct: mongo_direct) end |
#privately_writable(mongo_direct: nil) ⇒ Parse::Query Also known as: master_key_write_only
Find objects with no write permissions (master key only). Matches objects where _wperm is empty or doesn't exist.
5581 5582 5583 |
# File 'lib/parse/query.rb', line 5581 def privately_writable(mongo_direct: nil) writable_by("none", mongo_direct: mongo_direct) end |
#publicly_readable(mongo_direct: nil) ⇒ Parse::Query
Find objects that are publicly readable (anyone can read). Matches objects where _rperm contains "*".
5543 5544 5545 |
# File 'lib/parse/query.rb', line 5543 def publicly_readable(mongo_direct: nil) readable_by("*", mongo_direct: mongo_direct) end |
#publicly_writable(mongo_direct: nil) ⇒ Parse::Query
Find objects that are publicly writable (anyone can write). Matches objects where _wperm contains "*". Useful for security audits to find potentially insecure objects.
5555 5556 5557 |
# File 'lib/parse/query.rb', line 5555 def publicly_writable(mongo_direct: nil) writable_by("*", mongo_direct: mongo_direct) end |
#raw { ... } ⇒ Array<Hash>
Returns raw unprocessed results from the query (hash format)
2028 2029 2030 |
# File 'lib/parse/query.rb', line 2028 def raw(&block) results(raw: true, &block) end |
#read_pref(preference) ⇒ self
Set the MongoDB read preference for this query. This allows directing read queries to secondary replicas for load balancing.
849 850 851 852 |
# File 'lib/parse/query.rb', line 849 def read_pref(preference) @read_preference = preference self end |
#readable_by(permission, mongo_direct: nil) ⇒ Parse::Query
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.
5471 5472 5473 5474 5475 |
# File 'lib/parse/query.rb', line 5471 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).
5486 5487 5488 5489 5490 |
# File 'lib/parse/query.rb', line 5486 def readable_by_role(role_name, mongo_direct: nil) @acl_query_mongo_direct = mongo_direct unless mongo_direct.nil? where(:ACL.readable_by_role => role_name) self end |
#related_to(field, pointer) ⇒ Object
879 880 881 882 883 |
# File 'lib/parse/query.rb', line 879 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
4216 4217 4218 |
# File 'lib/parse/query.rb', line 4216 def requires_aggregation? !pipeline.empty? end |
#requires_aggregation_pipeline? ⇒ Boolean
Check if this query contains constraints that require aggregation pipeline processing
2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 |
# File 'lib/parse/query.rb', line 2009 def requires_aggregation_pipeline? return false if @where.empty? # Markers (including __aggregation_pipeline) are stripped from the # public compile_where path; consult the marker view explicitly. markers = compile_markers # Check if the marker hash itself has aggregation pipeline marker return true if markers.key?("__aggregation_pipeline") # Check if any of the constraint values has aggregation pipeline marker markers.values.any? { |constraint| constraint.is_a?(Hash) && constraint.key?("__aggregation_pipeline") } end |
#requires_mongo_direct? ⇒ Boolean
Check if this query contains a constraint that can only be answered
via mongo-direct (e.g. $geoIntersects with a full $geometry
against a non-GeoPoint column — an operator Parse Server's REST
find layer does not expose). Direct-only constraints emit a
"__mongo_direct_only" marker which this predicate detects.
1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 |
# File 'lib/parse/query.rb', line 1782 def requires_mongo_direct? return false if @where.empty? # Read from the un-stripped marker hash — `compile_where` removes # `__`-prefixed routing markers before they ship to Parse / Mongo. markers = compile_markers return true if markers.key?("__mongo_direct_only") markers.values.any? do |constraint| constraint.is_a?(Hash) && constraint.key?("__mongo_direct_only") end end |
#result_pointers { ... } ⇒ Array<Parse::Pointer> Also known as: results_pointers
Returns only pointer objects for all matching results This is memory efficient for large result sets where you only need pointers
2036 2037 2038 |
# File 'lib/parse/query.rb', line 2036 def result_pointers(&block) results(return_pointers: true, &block) end |
#results(raw: false, return_pointers: false, mongo_direct: false) { ... } ⇒ Array<Hash>, Array<Parse::Object> Also known as: result
Executes the query and builds the result set of Parse::Objects that matched. When this method is passed a block, the block is yielded for each matching item in the result, and the items are not returned. This methodology is more performant as large quantifies of objects are fetched in batches and all of them do not have to be kept in memory after the query finishes executing. This is the recommended method of processing large result sets.
1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 |
# File 'lib/parse/query.rb', line 1706 def results(raw: false, return_pointers: false, mongo_direct: false, &block) # Use direct MongoDB query if requested if mongo_direct return results_direct(raw: raw, **mongo_direct_auth_kwargs, &block) end # Auto-route to mongo-direct when the compiled where contains a # constraint that Parse Server's REST find layer cannot express # (e.g. $geoIntersects with a full $geometry against a non-Point # column). Mirrors the existing aggregation auto-route at line # ~1321 below — the constraint emits a marker, the query layer # detects it, and routing happens transparently. The auth # context (use_master_key, scope_to_user, or session_token) # decides how ACL simulation runs through mongo-direct. if requires_mongo_direct? assert_mongo_direct_routable! return results_direct(raw: raw, **mongo_direct_auth_kwargs, &block) end if @results.nil? if block_given? max_results(raw: raw, return_pointers: return_pointers, &block) elsif @limit.is_a?(Numeric) || requires_aggregation_pipeline? # Check if this query requires aggregation pipeline processing if requires_aggregation_pipeline? # Use Aggregation class which handles both Parse Server and MongoDB direct aggregation = execute_aggregation_pipeline if raw items = aggregation.raw elsif return_pointers items = to_pointers(aggregation.raw) else items = aggregation.results end return items.each(&block) if block_given? @results = items else response = fetch!(compile) return [] if response.error? items = if raw response.results elsif return_pointers to_pointers(response.results) else decode(response.results) end return items.each(&block) if block_given? @results = items end else @results = max_results(raw: raw, return_pointers: return_pointers) end end @results end |
#results_direct(raw: false, max_time_ms: nil, session_token: nil, master: nil, acl_user: nil, acl_role: nil) { ... } ⇒ Array<Parse::Object>, Array<Hash>
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.
2065 2066 2067 2068 2069 2070 2071 2072 2073 2074 2075 2076 2077 2078 2079 2080 2081 2082 2083 2084 2085 2086 2087 2088 2089 2090 2091 2092 2093 2094 2095 2096 2097 2098 2099 2100 2101 2102 2103 2104 2105 2106 2107 2108 2109 2110 2111 2112 2113 2114 2115 2116 2117 2118 2119 2120 2121 2122 2123 2124 2125 2126 2127 2128 2129 2130 |
# File 'lib/parse/query.rb', line 2065 def results_direct(raw: false, max_time_ms: nil, session_token: nil, master: nil, acl_user: nil, acl_role: nil, &block) require_relative "mongodb" Parse::MongoDB.require_gem! unless Parse::MongoDB.available? raise Parse::MongoDB::NotEnabled, "Direct MongoDB queries are not enabled. " \ "Call Parse::MongoDB.configure(uri: 'mongodb://...', enabled: true) first." end # Build the aggregation pipeline for direct MongoDB execution pipeline = build_direct_mongodb_pipeline # When no explicit auth kwargs are provided by the caller, derive them # from the query's own auth state (session_token, acl_user, acl_role, or # master key) via mongo_direct_auth_kwargs — exactly the same fallback # used by distinct_direct, count_direct, and the requires_mongo_direct? # auto-route in results(). Without this, a plain .results_direct call on # a master-key client would resolve as anonymous and have the ACL match # stage filter out every row whose _rperm is [] (the default for objects # created without an explicit public-read ACL). if session_token.nil? && master.nil? && acl_user.nil? && acl_role.nil? auth = mongo_direct_auth_kwargs session_token = auth[:session_token] master = auth[:master] acl_user = auth[:acl_user] acl_role = auth[:acl_role] end # Execute the aggregation directly on MongoDB. The pipeline was built # entirely from SDK constraint translation (no user-supplied stages), # so legitimate +_rperm+/+_wperm+ references emitted by # {#readable_by_role} and friends are sanctioned. The DENIED_OPERATORS # walk still runs at the MongoDB layer. When `session_token:` or # `master:` is supplied, Parse::MongoDB.aggregate adds the # three-layer ACL simulation (top-level $match, $lookup rewriter, # post-fetch redactor) before/after the pipeline executes. raw_results = Parse::MongoDB.aggregate(@table, pipeline, max_time_ms: max_time_ms, allow_internal_fields: true, session_token: session_token, master: master, acl_user: acl_user, acl_role: acl_role, read_preference: @read_preference, hint: @hint) # Convert MongoDB documents to Parse format parse_results = Parse::MongoDB.convert_documents_to_parse(raw_results, @table) # Honor exclude_keys on the mongo-direct path: the pipeline can only # project the keys allowlist, so apply the denylist here as a post-fetch # sanitize over the Parse-format hashes (before the raw/decode fork so # both shapes are redacted). Does not alter the MongoDB query. redact_excluded_keys!(parse_results) if raw return parse_results.each(&block) if block_given? return parse_results end # Convert to Parse objects items = decode(parse_results) return items.each(&block) if block_given? items end |
#rewrite_expression_for_direct_mongodb(expr) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Recursively rewrite field references inside an aggregation expression to their direct-MongoDB column names.
Walks Strings, Arrays, and Hashes:
- A String starting with
$(but not$$, which denotes 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.
3135 3136 3137 3138 3139 3140 3141 3142 3143 3144 3145 3146 3147 3148 3149 3150 3151 3152 3153 3154 3155 3156 3157 3158 3159 |
# File 'lib/parse/query.rb', line 3135 def rewrite_expression_for_direct_mongodb(expr) case expr when String return expr unless expr.start_with?("$") # $$varName (let bindings) and $$ROOT / $$CURRENT / $$NOW etc. return expr if expr.start_with?("$$") # Split off the root path segment so `$user.name` rewrites only # the root: `$_p_user.name`. Internal helper handles _p_* and # built-in passthroughs idempotently. head, sep, tail = expr[1..-1].partition(".") "$#{convert_field_for_direct_mongodb(head)}#{sep}#{tail}" when Array expr.map { |e| rewrite_expression_for_direct_mongodb(e) } when Hash result = {} expr.each do |k, v| # `$literal` wraps a string constant; its argument is not a # field reference and must be preserved verbatim. result[k] = k.to_s == "$literal" ? v : rewrite_expression_for_direct_mongodb(v) end result else expr end end |
#scope_to_role(role) ⇒ self
Role-based ACL scoping for service-account-style queries that
need "what would a user holding this role see" without minting a
session token or naming a specific user. The SDK uses
Parse::Role#all_parent_role_names to expand the role's
inheritance chain so passing "scope:admin" includes any role
"scope:admin" inherits from (e.g. "scope:user").
The resulting permission set is ["*", "role:<name>", ...] —
no user_id slot. Documents whose _rperm would only grant a
specific user (and not any of the role names) are filtered out
of both the top-level result set and embedded sub-documents.
Same routing rules as #scope_to_user: the query auto-routes
through mongo-direct when the where clause contains a
direct-only constraint, and the three-layer ACL simulation
(top-level $match, $lookup rewriter, post-fetch redactor)
runs through ACLScope.
1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 |
# File 'lib/parse/query.rb', line 1878 def scope_to_role(role) unless role.is_a?(Parse::Role) || role.is_a?(String) || role.is_a?(Symbol) raise ArgumentError, "[Parse::Query] scope_to_role requires a Parse::Role or role-name String." end # Normalize Symbol at the boundary so downstream # Parse::ACLScope#resolve_for_role only ever sees Parse::Role or # String. Without normalization, any String-only operation # (e.g. #start_with?, #sub) silently NoMethodErrors on Symbol. @acl_role = role.is_a?(Symbol) ? role.to_s : role self end |
#scope_to_user(user) ⇒ self
Scope a query to a specific user's row-level ACL when it auto-routes
through mongo-direct. The SDK records the user, computes the
effective _rperm allow-set (user objectId + "*" + every role
name the user inherits via Role.all_for_user), and prepends
a { _rperm: { $in: ... } } $match to the mongo-direct pipeline
at execution time.
What this does NOT replicate: class-level permissions (CLP),
anonymous-user public-access nuances, beforeFind/afterFind
cloud triggers, or any field-level redaction Parse Server might
otherwise apply. This is a row-ACL floor, not full enforcement
parity with the Parse Server REST path. The intended use case is
"I need this mongo-direct-only query from a session-tokened
context, and I accept the row-ACL floor as my filter."
Edge case — objects with missing _rperm: Parse Server only
writes _rperm when an explicit ACL is applied; rows saved with
master-key access and no explicit ACL leave the field unset.
The injected filter is {$or: [{_rperm: {$exists: false}},
{_rperm: {$in: perms}}]}, treating missing-_rperm rows as
public-readable. Apps that store row-level ACL on every object
are unaffected by this fallback; apps that mix ACL'd and
public-default rows will see both classes of row through the
scoped query.
The query MUST still satisfy #assert_mongo_direct_routable! —
either use_master_key: true OR scope_to_user is set. A call to
scope_to_user is treated as opt-in to mongo-direct routing for
the direct-only constraints in the where clause.
1843 1844 1845 1846 1847 1848 |
# File 'lib/parse/query.rb', line 1843 def scope_to_user(user) raise ArgumentError, "[Parse::Query] scope_to_user requires a Parse::User or User Pointer." \ unless user.respond_to?(:id) && user.id.is_a?(String) @acl_user = user self end |
#select { ... } ⇒ Array
1350 1351 1352 1353 |
# File 'lib/parse/query.rb', line 1350 def select(&block) return results.enum_for(:select) unless block_given? # Sparkling magic! results.select(&block) end |
#skip(amount) ⇒ self
Use with limit to paginate through results. Default is 0.
778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 |
# File 'lib/parse/query.rb', line 778 def skip(amount) coerced = case amount when nil then 0 when Numeric then amount.to_i when String unless amount =~ /\A-?\d+\z/ raise ArgumentError, "Invalid skip #{amount.inspect}. Expected an Integer, " \ "a numeric String, or nil." end amount.to_i else raise ArgumentError, "Invalid skip #{amount.inspect}. Expected an Integer, " \ "a numeric String, or nil." end @skip = [0, coerced].max @results = nil self #chaining end |
#subscribe(fields: nil, keys: nil, watch: nil, session_token: nil, client: nil, use_master_key: false) {|subscription| ... } ⇒ Parse::LiveQuery::Subscription
Subscribe to real-time updates for objects matching this query. Uses Parse LiveQuery WebSocket connection to receive push notifications when objects are created, updated, deleted, or enter/leave the query results.
3230 3231 3232 3233 3234 3235 3236 3237 3238 3239 3240 3241 3242 3243 3244 |
# File 'lib/parse/query.rb', line 3230 def subscribe(fields: nil, keys: nil, watch: nil, session_token: nil, client: nil, use_master_key: false, &block) require_relative "live_query" lq_client = client || Parse::LiveQuery.client lq_client.subscribe( @table, where: compile_where, fields: fields, keys: keys, watch: watch, session_token: session_token || @session_token, use_master_key: use_master_key, &block ) end |
#sum(field) ⇒ Numeric
Calculate the sum of values for a specific field.
4229 4230 4231 4232 4233 4234 4235 4236 4237 4238 4239 4240 4241 4242 4243 |
# File 'lib/parse/query.rb', line 4229 def sum(field) if field.nil? || !field.respond_to?(:to_s) raise ArgumentError, "Invalid field name passed to `sum`." end # Format field name according to Parse conventions formatted_field = format_aggregation_field(field) # Build the aggregation pipeline pipeline = [ { "$group" => { "_id" => nil, "total" => { "$sum" => "$#{formatted_field}" } } }, ] execute_basic_aggregation(pipeline, "sum", field, "total") end |
#to_pointers(list, field = nil) ⇒ Array<Parse::Pointer>
Builds Parse::Pointer objects based on the set of Parse JSON hashes in an array.
4077 4078 4079 4080 4081 4082 4083 4084 4085 4086 4087 4088 4089 4090 4091 4092 4093 4094 4095 4096 4097 4098 4099 4100 4101 4102 4103 4104 4105 4106 4107 4108 4109 4110 4111 4112 |
# File 'lib/parse/query.rb', line 4077 def to_pointers(list, field = nil) list.map do |m| if field # Use schema-based conversion when field is provided converted = convert_pointer_value_with_schema(m, field, return_pointers: true) if converted.is_a?(Parse::Pointer) converted elsif m.is_a?(String) && m.include?("$") # Fallback to string parsing if schema conversion didn't work class_name, object_id = m.split("$", 2) if class_name && object_id Parse::Pointer.new(class_name, object_id) end else nil end else # Original logic for backward compatibility if m.is_a?(Hash) if m["__type"] == "Pointer" && m["className"] && m["objectId"] # Parse pointer object - use the className from the pointer Parse::Pointer.new(m["className"], m["objectId"]) elsif m["objectId"] # Standard Parse object with objectId - use the query table name Parse::Pointer.new(@table, m["objectId"]) end elsif m.is_a?(String) && m.include?("$") # Handle MongoDB pointer string format: "ClassName$objectId" class_name, object_id = m.split("$", 2) if class_name && object_id Parse::Pointer.new(class_name, object_id) end end end end.compact end |
#to_table(columns = nil, format: :ascii, headers: nil, sort_by: nil, sort_order: :asc) ⇒ String
Convert query results to a formatted table display.
4444 4445 4446 4447 4448 4449 4450 4451 4452 4453 4454 4455 4456 4457 4458 4459 4460 4461 4462 4463 4464 4465 4466 4467 4468 4469 4470 4471 4472 |
# File 'lib/parse/query.rb', line 4444 def to_table(columns = nil, format: :ascii, headers: nil, sort_by: nil, sort_order: :asc) objects = results return format_empty_table(format) if objects.empty? # Auto-detect columns if not provided if columns.nil? columns = auto_detect_columns(objects.first) end # Build table data table_data = build_table_data(objects, columns, headers) # Sort table data if sort_by is specified if sort_by sort_table_data!(table_data, sort_by, sort_order) end # Format based on requested format case format when :ascii format_ascii_table(table_data) when :csv format_csv_table(table_data) when :json format_json_table(table_data) else raise ArgumentError, "Unsupported format: #{format}. Use :ascii, :csv, or :json" end end |
#translate_pipeline_for_direct_mongodb(pipeline) ⇒ Array<Hash>
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Apply the direct-MongoDB stage converter to every stage in a pipeline.
Idempotent on already-translated input (the per-stage converter
passes _p_* references through unchanged).
3496 3497 3498 3499 |
# File 'lib/parse/query.rb', line 3496 def translate_pipeline_for_direct_mongodb(pipeline) return pipeline unless pipeline.is_a?(Array) pipeline.map { |stage| convert_stage_for_direct_mongodb(stage) } end |
#validate_no_where_operator!(hash) ⇒ Object
Retained for backwards compatibility. Use PipelineSecurity.validate_filter! for new code.
3527 3528 3529 3530 3531 |
# File 'lib/parse/query.rb', line 3527 def validate_no_where_operator!(hash) Parse::PipelineSecurity.validate_filter!(hash) rescue Parse::PipelineSecurity::Error => e raise ArgumentError, e. 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.
3517 3518 3519 3520 3521 |
# File 'lib/parse/query.rb', line 3517 def validate_pipeline!(pipeline) Parse::PipelineSecurity.validate_filter!(pipeline) rescue Parse::PipelineSecurity::Error => e raise ArgumentError, e. 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.
1036 1037 1038 1039 1040 1041 1042 1043 |
# File 'lib/parse/query.rb', line 1036 def where(expressions = nil, opts = {}) return @where if expressions.nil? if expressions.is_a?(Hash) # Route through conditions to handle special keywords like :keys, :include, etc. conditions(expressions) end self #chaining end |
#where_constraints ⇒ Hash
Formats the current set of Parse::Constraint instances in the where clause as an expression hash.
1018 1019 1020 |
# File 'lib/parse/query.rb', line 1018 def where_constraints @where.reduce({}) { |memo, constraint| memo[constraint.operation] = constraint.value; memo } end |
#writable_by(permission, mongo_direct: nil) ⇒ Parse::Query
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.
5510 5511 5512 5513 5514 |
# File 'lib/parse/query.rb', line 5510 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).
5525 5526 5527 5528 5529 |
# File 'lib/parse/query.rb', line 5525 def writable_by_role(role_name, mongo_direct: nil) @acl_query_mongo_direct = mongo_direct unless mongo_direct.nil? where(:ACL.writable_by_role => role_name) self end |
#|(other_query) ⇒ Query
Returns the combined query with an OR clause.
1091 1092 1093 1094 1095 1096 |
# File 'lib/parse/query.rb', line 1091 def |(other_query) raise ArgumentError, "Parse queries must be of the same class #{@table}." unless @table == other_query.table copy_query = self.clone copy_query.or_where other_query.where copy_query end |