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 # => {"fieldOne"=>1, "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 # => {"field_one"=>1, "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 # => {"FieldOne"=>1, "FieldTwo"=>2, "FieldThree"=>3}
Most of the constraints supported by Parse are available to ‘Parse::Query`. Assuming you have a column named `field`, here are some examples. For an explanation of the constraints, please see Parse Query Constraints documentation. You can build your own custom query constraints by creating a `Parse::Constraint` subclass. For all these `where` clauses assume `q` is a `Parse::Query` object.
Defined Under Namespace
Classes: MongoDirectRequired, PointerShapeError
Constant Summary collapse
- QUERY_OPTION_KEYS =
The set of symbol keys that #conditions treats as query-shape options (cache TTL, ordering, limits, ACL convenience helpers, session/master-key overrides) rather than as field-name constraints. External callers that need to partition a user-supplied constraints Hash into “real constraints vs query options” — most notably ‘Parse::Object.first_or_create!` and `Parse::Object.create_or_update!`, which must hand a Hash containing ONLY constraint key/value pairs to `Parse::CreateLock.canonicalize_attrs` — consult this set via option_key?.
Keep this list in sync with the option branches at the top of #conditions. Anything ‘conditions()` extracts as a query parameter rather than a constraint belongs here.
[ :order, :keys, :key, :skip, :limit, :include, :includes, :cache, :use_master_key, :session, :read_preference, :readable_by, :writable_by, :readable_by_role, :writable_by_role, :publicly_readable, :publicly_writable, :privately_readable, :master_key_read_only, :privately_writable, :master_key_write_only, :private_acl, :master_key_only, :not_publicly_readable, :not_publicly_writable, ].to_set.freeze
- BLOCKED_PIPELINE_STAGES =
Deprecated.
Retained for backwards compatibility. The canonical list now lives in PipelineSecurity::DENIED_OPERATORS and is enforced recursively, not only at the top-level stage.
Create an Aggregation object for executing arbitrary MongoDB pipelines Pipeline stages that are blocked to prevent data exfiltration or destructive operations.
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 ‘select` queries where you have to pass a `key` parameter for matching different tables.
-
#read_preference ⇒ Symbol, String
Set the MongoDB read preference for this query.
-
#session_token ⇒ Object
Returns the value of attribute session_token.
-
#table ⇒ String
The name of the Parse collection to query against.
-
#use_master_key ⇒ Boolean
True or false on whether we should send the master key in this request.
-
#verbose_aggregate ⇒ Object
Returns the value of attribute verbose_aggregate.
Class Method Summary collapse
-
.all(table, constraints = { limit: :max }) ⇒ Query
Helper method to create a query with constraints for a specific Parse collection.
-
.and(*queries) ⇒ Parse::Query
Combines multiple queries with AND logic using full pipeline approach Each query’s complete constraint set is ANDed together.
-
.compile_markers(where) ⇒ Hash
Return the un-stripped reduced hash so the routing/pipeline layer can inspect ‘__`-prefixed markers (e.g. `“__mongo_direct_only”`, `“__aggregation_pipeline”`).
-
.compile_where(where) ⇒ Hash
This methods takes a set of constraints and merges them to build a final ‘where` constraint clause for sending to the Parse backend.
-
.format_field(str) ⇒ String
Formatted string using Query.field_formatter.
-
.known_parse_classes ⇒ Object
Known Parse classes for fast validation - dynamically loaded from schema.
-
.option_key?(key) ⇒ Boolean
Whether ‘key` is one of the QUERY_OPTION_KEYS that #conditions absorbs as a query-shape option rather than a field-name constraint.
-
.or(*queries) ⇒ Parse::Query
Combines multiple queries with OR logic using full pipeline approach Each query’s complete constraint set becomes one branch of the OR condition.
-
.parse_keys_to_nested_keys(keys) ⇒ Hash
Parses keys patterns to build a map of nested fetched keys.
-
.pointer_shape_warned ⇒ Object
Process-wide ‘[table, field]` cache for warn-once dedup in #handle_unresolvable_pointer_in_array!.
-
.reset_known_parse_classes! ⇒ Object
Allow resetting the cached known classes (useful for testing).
-
.to_snake_case(str) ⇒ String
Convert camelCase string to snake_case.
Instance Method Summary collapse
-
#add_constraint(operator, value = nil, opts = {}) ⇒ self
Add a constraint to the query.
-
#add_constraints(list) ⇒ self
Combine a list of Constraint objects.
-
#after_prepare { ... } ⇒ Object
A callback called after the query is compiled.
- #aggregate(pipeline, verbose: nil, mongo_direct: nil, rewrite_lookups: nil) ⇒ Object (also: #aggregate_pipeline)
-
#aggregate_from_query(additional_stages = [], verbose: nil, mongo_direct: nil) ⇒ Aggregation
Converts the current query into an aggregate pipeline and executes it.
-
#all(expressions = { limit: :max }) { ... } ⇒ Array<Hash>, Array<Parse::Object>
Similar to #results but takes an additional set of conditions to apply.
- #as_json(*args) ⇒ Hash
-
#atlas_autocomplete(query, field:, **options) ⇒ Parse::AtlasSearch::AutocompleteResult
Execute an autocomplete search using MongoDB Atlas Search.
-
#atlas_facets(query, facets, **options) ⇒ Parse::AtlasSearch::FacetedResult
Execute a faceted search using MongoDB Atlas Search.
-
#atlas_search(query = nil, **options) {|SearchBuilder| ... } ⇒ Parse::AtlasSearch::SearchResult
Execute a full-text search using MongoDB Atlas Search.
-
#average(field) ⇒ Float
(also: #avg)
Calculate the average of values for a specific field.
-
#before_prepare { ... } ⇒ Object
A callback called before the query is compiled.
-
#build_aggregation_pipeline ⇒ Array
Build the complete aggregation pipeline from constraints Pipeline order: $match (regular) -> $lookup (subqueries) -> $match (post-lookup) -> $match (aggregation) -> non-$match stages -> limit/skip.
-
#build_direct_mongodb_pipeline ⇒ Array<Hash>
private
Build an aggregation pipeline optimized for direct MongoDB execution.
-
#build_filter_condition(where) ⇒ Hash
Build a $filter condition expression from where constraints.
-
#build_include_lookup_stages(includes) ⇒ Array<Hash>
private
Build $lookup stages for included pointer fields in direct MongoDB queries.
-
#clause(clause_name = :where) ⇒ Object
returns the query clause for the particular clause.
-
#clear(item = :results) ⇒ self
Clear a specific clause of this query.
-
#clone ⇒ Parse::Query
Creates a deep copy of this query object, allowing independent modifications.
-
#compile(encode: true, includeClassName: false) ⇒ Hash
Complies the query and runs all prepare callbacks.
-
#compile_where ⇒ Hash
A hash representing just the ‘where` clause of this query, with SDK-internal routing markers stripped.
-
#conditions(expressions = {}) ⇒ self
(also: #query, #append)
Add a set of query expressions and constraints.
- #constraints(raw = false) ⇒ Array<Parse::Constraint>, Hash
-
#convert_addfields_for_direct_mongodb(spec) ⇒ Object
private
Convert a $addFields / $set stage for direct MongoDB.
-
#convert_constraints_for_direct_mongodb(constraints) ⇒ Hash
private
Convert constraints for direct MongoDB execution.
-
#convert_field_for_direct_mongodb(field) ⇒ String
private
Convert a field name for direct MongoDB access.
-
#convert_group_for_direct_mongodb(group) ⇒ Object
private
Convert $group stage for direct MongoDB.
-
#convert_match_for_direct_mongodb(match) ⇒ Object
private
Convert a $match stage for direct MongoDB.
-
#convert_projection_for_direct_mongodb(projection) ⇒ Object
private
Convert projection fields for direct MongoDB.
-
#convert_replace_root_for_direct_mongodb(spec) ⇒ Object
private
Convert a $replaceRoot stage for direct MongoDB.
-
#convert_sort_for_direct_mongodb(sort) ⇒ Object
private
Convert sort specification for direct MongoDB.
-
#convert_stage_for_direct_mongodb(stage) ⇒ Hash
private
Convert an aggregation stage for direct MongoDB execution.
-
#convert_value_for_direct_mongodb(field, value) ⇒ Object
private
Convert a value for direct MongoDB execution.
-
#count(mongo_direct: false) ⇒ Integer
Perform a count query.
-
#count_direct(session_token: nil, master: nil, acl_user: nil, acl_role: nil) ⇒ Integer
Execute a count query directly against MongoDB, bypassing Parse Server.
-
#count_distinct(field) ⇒ Integer
Perform a count distinct query using MongoDB aggregation pipeline.
-
#cursor(limit: 100, order: nil) ⇒ Parse::Cursor
Create a cursor-based paginator for efficiently traversing large datasets.
-
#decode(list) ⇒ Array<Parse::Object>
Builds objects based on the set of Parse JSON hashes in an array.
-
#deduplicate_consecutive_match_stages(pipeline) ⇒ Array<Hash>
private
Merge consecutive $match stages in an aggregation pipeline.
-
#distinct(field, return_pointers: false, mongo_direct: false, order: nil) ⇒ Object
Queries can be made using distinct, allowing you find unique values for a specified field.
-
#distinct_direct(field, return_pointers: false, order: nil, session_token: nil, master: nil, acl_user: nil, acl_role: nil) ⇒ Array
Execute a distinct query directly against MongoDB, bypassing Parse Server.
-
#distinct_direct_pointers(field, order: nil, session_token: nil, master: nil, acl_user: nil, acl_role: nil) ⇒ Array
Convenience method for distinct_direct that always returns Parse::Pointer objects for pointer fields.
-
#distinct_objects(field, return_pointers: false) ⇒ Array
Enhanced distinct method that automatically populates Parse pointer objects at the server level.
-
#distinct_pointers(field, order: nil) ⇒ Array
Convenience method for distinct queries that always return Parse::Pointer objects for pointer fields.
-
#distinct_query_is_scoped? ⇒ Boolean
private
Whether this query carries a non-master-key auth scope.
- #each { ... } ⇒ Array
-
#execute_aggregation_pipeline ⇒ Aggregation
Execute an aggregation pipeline for queries with pipeline constraints.
-
#explain ⇒ Hash
Returns the query execution plan from MongoDB.
-
#extract_subquery_to_lookup_stages(constraints) ⇒ Hash
Extract $inQuery and $notInQuery constraints and build $lookup stages for them.
-
#fetch!(compiled_query) ⇒ Parse::Response
(also: #execute!)
Performs the fetch request for the query.
- #first(limit_or_constraints = 1, mongo_direct: false, **options) ⇒ Object
-
#first_direct(limit_or_constraints = 1) ⇒ Parse::Object, ...
Execute the query directly against MongoDB and return the first result.
-
#get(object_id) ⇒ Parse::Object
Retrieve a single object by its objectId.
-
#get_pointer_target_class(field) ⇒ String?
private
Get the target class name for a pointer field from model references.
-
#group_by(field, flatten_arrays: false, sortable: false, return_pointers: false, mongo_direct: false) ⇒ GroupBy, SortableGroupBy
Group results by a specific field and return a GroupBy object for chaining aggregations.
-
#group_by_date(field, interval, sortable: false, return_pointers: false, timezone: nil, mongo_direct: false) ⇒ GroupByDate, SortableGroupByDate
Group results by a date field at specified time intervals.
-
#group_objects_by(field, return_pointers: false) ⇒ Hash
Group Parse objects by a field value and return arrays of actual objects.
-
#has_subquery_constraints?(constraints) ⇒ Boolean
Check if constraints contain $inQuery or $notInQuery that need resolution.
-
#include(*fields) ⇒ Object
alias for includes.
-
#includes(*fields) ⇒ self
Set a list of Parse Pointer columns to be fetched for matching records.
-
#initialize(table, constraints = {}) ⇒ Query
constructor
Constructor method to create a query with constraints for a specific Parse collection.
-
#keys(*fields) ⇒ self
(also: #select_fields)
Restrict the fields returned by the query.
-
#last_updated(limit = 1, **options) ⇒ Parse::Object+
Returns the most recently updated object(s) (ordered by updated_at descending).
-
#latest(limit = 1, **options) ⇒ Parse::Object+
Returns the most recently created object(s) (ordered by created_at descending).
-
#limit(count) ⇒ self
Limit the number of objects returned by the query.
- #map { ... } ⇒ Array
-
#max(field) ⇒ Object
Find the maximum value for a specific field.
-
#min(field) ⇒ Object
Find the minimum value for a specific field.
-
#not_publicly_readable(mongo_direct: nil) ⇒ Parse::Query
Find objects that are NOT publicly readable.
-
#not_publicly_writable(mongo_direct: nil) ⇒ Parse::Query
Find objects that are NOT publicly writable.
-
#or_where(where_clauses = []) ⇒ Query
Combine two where clauses into an OR constraint.
-
#order(*ordering) ⇒ self
Add a sorting order for the query.
-
#pipeline ⇒ Array
Returns the aggregation pipeline for this query if it contains pipeline-based constraints.
-
#pipeline_uses_internal_fields?(pipeline) ⇒ Boolean
Check if the pipeline references internal Parse fields that require MongoDB direct access.
-
#pluck(field) ⇒ Array
Extract values for a specific field from all matching objects.
-
#prepared(includeClassName: false) ⇒ Hash
Returns a compiled query without encoding the where clause.
-
#pretty ⇒ String
Retruns a formatted JSON string representing the query, useful for debugging.
-
#private_acl(mongo_direct: nil) ⇒ Parse::Query
(also: #master_key_only)
Find objects with completely private ACL (no read AND no write permissions).
-
#privately_readable(mongo_direct: nil) ⇒ Parse::Query
(also: #master_key_read_only)
Find objects with no read permissions (master key only).
-
#privately_writable(mongo_direct: nil) ⇒ Parse::Query
(also: #master_key_write_only)
Find objects with no write permissions (master key only).
-
#publicly_readable(mongo_direct: nil) ⇒ Parse::Query
Find objects that are publicly readable (anyone can read).
-
#publicly_writable(mongo_direct: nil) ⇒ Parse::Query
Find objects that are publicly writable (anyone can write).
-
#raw { ... } ⇒ Array<Hash>
Returns raw unprocessed results from the query (hash format).
-
#read_pref(preference) ⇒ self
Set the MongoDB read preference for this query.
-
#readable_by(permission, mongo_direct: nil) ⇒ Parse::Query
Filter by ACL read permissions using exact permission strings.
-
#readable_by_role(role_name, mongo_direct: nil) ⇒ Parse::Query
Filter by ACL read permissions using role names (adds “role:” prefix).
- #related_to(field, pointer) ⇒ Object
-
#requires_aggregation? ⇒ Boolean
Check if this query requires aggregation pipeline execution.
-
#requires_aggregation_pipeline? ⇒ Boolean
Check if this query contains constraints that require aggregation pipeline processing.
-
#requires_mongo_direct? ⇒ Boolean
Check if this query contains a constraint that can only be answered via mongo-direct (e.g. ‘$geoIntersects` with a full `$geometry` against a non-GeoPoint column — an operator Parse Server’s REST find layer does not expose).
-
#result_pointers { ... } ⇒ Array<Parse::Pointer>
(also: #results_pointers)
Returns only pointer objects for all matching results This is memory efficient for large result sets where you only need pointers.
-
#results(raw: false, return_pointers: false, mongo_direct: false) { ... } ⇒ Array<Hash>, Array<Parse::Object>
(also: #result)
Executes the query and builds the result set of Parse::Objects that matched.
-
#results_direct(raw: false, max_time_ms: nil, session_token: nil, master: nil, acl_user: nil, acl_role: nil) { ... } ⇒ Array<Parse::Object>, Array<Hash>
Execute the query directly against MongoDB, bypassing Parse Server.
-
#rewrite_expression_for_direct_mongodb(expr) ⇒ Object
private
Recursively rewrite field references inside an aggregation expression to their direct-MongoDB column names.
-
#scope_to_role(role) ⇒ self
Role-based ACL scoping for service-account-style queries that need “what would a user holding this role see” without minting a session token or naming a specific user.
-
#scope_to_user(user) ⇒ self
Scope a query to a specific user’s row-level ACL when it auto-routes through mongo-direct.
- #select { ... } ⇒ Array
-
#skip(amount) ⇒ self
Use with limit to paginate through results.
-
#subscribe(fields: nil, session_token: nil, client: nil) ⇒ Parse::LiveQuery::Subscription
Subscribe to real-time updates for objects matching this query.
-
#sum(field) ⇒ Numeric
Calculate the sum of values for a specific field.
- #to_a ⇒ Array
-
#to_pointers(list, field = nil) ⇒ Array<Parse::Pointer>
Builds Parse::Pointer objects based on the set of Parse JSON hashes in an array.
-
#to_table(columns = nil, format: :ascii, headers: nil, sort_by: nil, sort_order: :asc) ⇒ String
Convert query results to a formatted table display.
-
#translate_pipeline_for_direct_mongodb(pipeline) ⇒ Array<Hash>
private
Apply the direct-MongoDB stage converter to every stage in a pipeline.
-
#validate_no_where_operator!(hash) ⇒ Object
deprecated
Deprecated.
Retained for backwards compatibility. Use PipelineSecurity.validate_filter! for new code.
-
#validate_pipeline!(pipeline) ⇒ Object
Validates that a pipeline does not contain dangerous operators.
-
#where(expressions = nil, opts = {}) ⇒ self
Add additional query constraints to the ‘where` clause.
-
#where_constraints ⇒ Hash
Formats the current set of Parse::Constraint instances in the where clause as an expression hash.
-
#writable_by(permission, mongo_direct: nil) ⇒ Parse::Query
Filter by ACL write permissions using exact permission strings.
-
#writable_by_role(role_name, mongo_direct: nil) ⇒ Parse::Query
Filter by ACL write permissions using role names (adds “role:” prefix).
-
#|(other_query) ⇒ Query
The combined query with an OR clause.
Constructor Details
#new(table) ⇒ Query #new(parseSubclass) ⇒ Query
Constructor method to create a query with constraints for a specific Parse collection. Also sets the default limit count to ‘:max`.
448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 |
# File 'lib/parse/query.rb', line 448 def initialize(table, constraints = {}) table = table.to_s.to_parse_class if table.is_a?(Symbol) table = table.parse_class if table.respond_to?(:parse_class) raise ArgumentError, "First parameter should be the name of the Parse class (table)" unless table.is_a?(String) @count = 0 #non-zero/1 implies a count query request @where = [] @order = [] @keys = [] @includes = [] @limit = nil @skip = 0 @table = table @cache = Parse.default_query_cache @use_master_key = true @verbose_aggregate = false conditions constraints end |
Class Attribute Details
.allow_scope_introspection ⇒ Symbol
The method to use when converting field names to Parse column names. Default is String#columnize. By convention Parse uses lowercase-first camelcase syntax for field/column names, but ruby uses snakecase. To support this methodology we process all field constraints through the method defined by the field formatter. You may set this to nil to turn off this functionality.
|
|
# File 'lib/parse/query.rb', line 254
|
.field_formatter ⇒ Symbol
The method to use when converting field names to Parse column names. Default is String#columnize. By convention Parse uses lowercase-first camelcase syntax for field/column names, but ruby uses snakecase. To support this methodology we process all field constraints through the method defined by the field formatter. You may set this to nil to turn off this functionality.
268 269 270 |
# File 'lib/parse/query.rb', line 268 def field_formatter @field_formatter end |
Instance Attribute Details
#acl_role ⇒ Parse::Role, ... (readonly)
Returns the role the query was scoped to via #scope_to_role, or nil.
1626 1627 1628 |
# File 'lib/parse/query.rb', line 1626 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.
1622 1623 1624 |
# File 'lib/parse/query.rb', line 1622 def acl_user @acl_user end |
#cache ⇒ Boolean, Integer
Set whether this query should be cached and for how long. This parameter is used to cache queries when using Middleware::Caching. If the caching middleware is configured, all queries will be cached for the duration allowed by the cache, and therefore some queries could return cached results. To disable caching and cached results for this specific query, you may set this field to ‘false`. To specify the specific amount of time you want this query to be cached, set a duration (in number of seconds) that the caching middleware should cache this request.
183 |
# File 'lib/parse/query.rb', line 183 attr_reader :table, :session_token |
#client ⇒ Parse::Client
Returns the client to use for making the API request.
183 |
# File 'lib/parse/query.rb', line 183 attr_reader :table, :session_token |
#key ⇒ String
This parameter is used to support ‘select` queries where you have to pass a `key` parameter for matching different tables.
183 |
# File 'lib/parse/query.rb', line 183 attr_reader :table, :session_token |
#read_preference ⇒ Symbol, String
Set the MongoDB read preference for this query. This allows directing read queries to secondary replicas for load balancing.
183 |
# File 'lib/parse/query.rb', line 183 attr_reader :table, :session_token |
#session_token ⇒ Object
Returns the value of attribute session_token.
183 |
# File 'lib/parse/query.rb', line 183 attr_reader :table, :session_token |
#table ⇒ String
Returns the name of the Parse collection to query against.
183 184 185 |
# File 'lib/parse/query.rb', line 183 def table @table end |
#use_master_key ⇒ Boolean
True or false on whether we should send the master key in this request. If You have provided the master_key when initializing Parse, then all requests will send the master key by default. This feature is useful when you want to make a particular query be performed with public credentials, or on behalf of a user using a #session_token. Default is set to true.
183 |
# File 'lib/parse/query.rb', line 183 attr_reader :table, :session_token |
#verbose_aggregate ⇒ Object
Returns the value of attribute verbose_aggregate.
185 186 187 |
# File 'lib/parse/query.rb', line 185 def verbose_aggregate @verbose_aggregate end |
Class Method Details
.all(table, constraints = { limit: :max }) ⇒ Query
Helper method to create a query with constraints for a specific Parse collection. Also sets the default limit count to ‘:max`.
331 332 333 |
# File 'lib/parse/query.rb', line 331 def all(table, constraints = { limit: :max }) self.new(table, constraints.reverse_merge({ limit: :max })) end |
.and(*queries) ⇒ Parse::Query
Combines multiple queries with AND logic using full pipeline approach Each query’s complete constraint set is ANDed together
5028 5029 5030 5031 5032 5033 5034 5035 5036 5037 5038 5039 5040 5041 5042 5043 5044 5045 5046 5047 5048 5049 5050 5051 5052 5053 5054 5055 5056 5057 5058 5059 |
# File 'lib/parse/query.rb', line 5028 def self.and(*queries) queries = queries.flatten.compact return nil if queries.empty? # Get the table from the first query table = queries.first.table # Ensure all queries are for the same table unless queries.all? { |q| q.table == table } raise ArgumentError, "All queries passed to Parse::Query.and must be for the same Parse class." end # Start with an empty query for this table result = self.new(table) # Filter to only queries that have constraints queries = queries.filter { |q| q.where.present? && !q.where.empty? } # Add each query's complete constraint set with AND logic # Multiple constraints in a query are implicitly ANDed together by Parse queries.each do |query| # Compile the where constraints to check if they result in empty conditions compiled_where = Parse::Query.compile_where(query.where) unless compiled_where.empty? # Directly append constraints to result's where array # (where method only accepts Hash, but query.where returns Array<Constraint>) result.instance_variable_get(:@where).concat(query.where) end end result end |
.compile_markers(where) ⇒ Hash
Return the un-stripped reduced hash so the routing/pipeline layer can inspect ‘__`-prefixed markers (e.g. `“__mongo_direct_only”`, `“__aggregation_pipeline”`). These markers are SDK-internal hints and must never be sent to Parse REST or MongoDB — that’s what compile_where is for.
357 358 359 |
# File 'lib/parse/query.rb', line 357 def compile_markers(where) constraint_reduce(where) end |
.compile_where(where) ⇒ Hash
This methods takes a set of constraints and merges them to build a final ‘where` constraint clause for sending to the Parse backend.
‘__`-prefixed internal routing markers (e.g. `“__mongo_direct_only”` and `“__aggregation_pipeline”`) are stripped from the returned hash —they are SDK-internal hints that must never reach Parse REST or MongoDB. Use compile_markers (instance method `#compile_markers`) to retrieve them for routing decisions / pipeline assembly.
346 347 348 |
# File 'lib/parse/query.rb', line 346 def compile_where(where) constraint_reduce(where).reject { |k, _| k.is_a?(String) && k.start_with?("__") } end |
.format_field(str) ⇒ String
Returns formatted string using field_formatter.
278 279 280 281 282 283 284 |
# File 'lib/parse/query.rb', line 278 def format_field(str) res = str.to_s.strip if field_formatter.present? && res.respond_to?(field_formatter) res = res.send(field_formatter) end res end |
.known_parse_classes ⇒ Object
Known Parse classes for fast validation - dynamically loaded from schema
73 74 75 76 77 78 79 80 81 82 83 84 85 |
# File 'lib/parse/query.rb', line 73 def self.known_parse_classes @known_parse_classes ||= begin # Get all classes from Parse schema response = Parse.client.schemas schema_classes = response.success? ? response.result.dig("results")&.map { |cls| cls["className"] } || [] : [] # Add built-in Parse classes built_in_classes = %w[_User _Role _Session _Installation _Audience User Role Session Installation Audience] (built_in_classes + schema_classes).uniq.freeze rescue # Fallback to built-in classes if schema query fails (e.g., during testing without server) %w[_User _Role _Session _Installation _Audience User Role Session Installation Audience].freeze end end |
.option_key?(key) ⇒ Boolean
QUERY_OPTION_KEYS must be kept in sync with the option-branch keys recognized at the top of #conditions. When adding a new query option, update BOTH places — this predicate is the public-facing source of truth for callers that partition ‘query_attrs` into constraints vs options (notably Object.first_or_create! and Object.create_or_update! for lock canonicalization), and the option-branch in `conditions` is what actually absorbs the option onto the query.
Whether ‘key` is one of the QUERY_OPTION_KEYS that #conditions absorbs as a query-shape option rather than a field-name constraint. Accepts Symbol or String; returns false for any other type (including `Parse::Operation`, which is always a constraint).
246 247 248 249 |
# File 'lib/parse/query.rb', line 246 def option_key?(key) return false unless key.is_a?(Symbol) || key.is_a?(String) QUERY_OPTION_KEYS.include?(key.to_sym) end |
.or(*queries) ⇒ Parse::Query
Combines multiple queries with OR logic using full pipeline approach Each query’s complete constraint set becomes one branch of the OR condition
4993 4994 4995 4996 4997 4998 4999 5000 5001 5002 5003 5004 5005 5006 5007 5008 5009 5010 5011 5012 5013 5014 5015 5016 5017 5018 5019 5020 5021 |
# File 'lib/parse/query.rb', line 4993 def self.or(*queries) queries = queries.flatten.compact return nil if queries.empty? # Get the table from the first query table = queries.first.table # Ensure all queries are for the same table unless queries.all? { |q| q.table == table } raise ArgumentError, "All queries passed to Parse::Query.or must be for the same Parse class." end # Start with an empty query for this table result = self.new(table) # Filter to only queries that have constraints queries = queries.filter { |q| q.where.present? && !q.where.empty? } # Add each query's complete constraint set as an OR branch queries.each do |query| # Compile the where constraints to check if they result in empty conditions compiled_where = Parse::Query.compile_where(query.where) unless compiled_where.empty? result.or_where(query.where) end end result end |
.parse_keys_to_nested_keys(keys) ⇒ Hash
Parses keys patterns to build a map of nested fetched keys. Handles arbitrary nesting depth (e.g., “a.b.c.d” creates entries for a, b, c). For example, [“project.name”, “project.status”, “author.email”] becomes: { project: [:name, :status], author: [:email] }
299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 |
# File 'lib/parse/query.rb', line 299 def parse_keys_to_nested_keys(keys) return {} if keys.nil? || keys.empty? nested_map = {} keys.each do |key_path| parts = key_path.to_s.split(".") # Skip keys without dots - they're top-level fields, not nested next if parts.length < 2 # Process each level of nesting # For path "a.b.c.d": a gets b, b gets c, c gets d parts.each_with_index do |part, index| field_name = part.to_sym nested_map[field_name] ||= [] # If there's a next part, add it to this field's nested keys if index < parts.length - 1 next_field = parts[index + 1].to_sym nested_map[field_name] << next_field unless nested_map[field_name].include?(next_field) end end end nested_map end |
.pointer_shape_warned ⇒ Object
Process-wide ‘[table, field]` cache for warn-once dedup in #handle_unresolvable_pointer_in_array!.
272 273 274 |
# File 'lib/parse/query.rb', line 272 def pointer_shape_warned @pointer_shape_warned ||= {} end |
.reset_known_parse_classes! ⇒ Object
Allow resetting the cached known classes (useful for testing)
88 89 90 |
# File 'lib/parse/query.rb', line 88 def self.reset_known_parse_classes! @known_parse_classes = nil end |
.to_snake_case(str) ⇒ String
Convert camelCase string to snake_case
289 290 291 |
# File 'lib/parse/query.rb', line 289 def to_snake_case(str) str.to_s.underscore end |
Instance Method Details
#add_constraint(operator, value = nil, opts = {}) ⇒ self
Add a constraint to the query. This is mainly used internally for compiling constraints.
821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 |
# File 'lib/parse/query.rb', line 821 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 @where.push constraint @results = nil self #chaining end |
#add_constraints(list) ⇒ self
Combine a list of Constraint objects
799 800 801 802 803 |
# File 'lib/parse/query.rb', line 799 def add_constraints(list) list = Array.wrap(list).select { |m| m.is_a?(Parse::Constraint) } @where = @where + list self end |
#after_prepare { ... } ⇒ Object
A callback called after the query is compiled
102 |
# File 'lib/parse/query.rb', line 102 define_model_callbacks :prepare, only: [:after, :before] |
#aggregate(pipeline, verbose: nil, mongo_direct: nil, rewrite_lookups: nil) ⇒ Object Also known as: aggregate_pipeline
2995 2996 2997 2998 2999 3000 3001 3002 3003 3004 3005 3006 3007 3008 3009 3010 3011 3012 3013 3014 3015 3016 3017 3018 3019 3020 3021 3022 3023 3024 3025 3026 3027 3028 3029 3030 3031 3032 3033 3034 3035 3036 3037 3038 3039 3040 3041 3042 3043 3044 3045 3046 3047 3048 3049 3050 3051 3052 3053 3054 3055 3056 3057 3058 3059 3060 3061 3062 3063 3064 3065 3066 3067 3068 3069 3070 3071 3072 3073 3074 3075 3076 3077 3078 3079 3080 3081 3082 3083 3084 3085 3086 3087 3088 3089 3090 3091 3092 3093 3094 3095 3096 3097 3098 3099 3100 3101 3102 3103 3104 3105 3106 3107 3108 3109 3110 3111 3112 3113 3114 3115 3116 3117 3118 3119 3120 3121 3122 3123 3124 3125 3126 3127 3128 3129 3130 3131 3132 3133 3134 3135 3136 3137 3138 3139 3140 |
# File 'lib/parse/query.rb', line 2995 def aggregate(pipeline, verbose: nil, mongo_direct: nil, rewrite_lookups: nil) validate_pipeline!(pipeline) # Auto-rewrite LLM-style $lookup stages against logical Parse class # names into the Parse-on-Mongo column form (_p_*/parseReference) when # the foreign class declares parse_reference. Idempotent on already- # rewritten input. Controlled by Parse.rewrite_lookups (default true) # or the per-call `rewrite_lookups:` kwarg. pipeline = Parse::LookupRewriter.auto_rewrite( pipeline, class_name: @table, enabled: rewrite_lookups, ) # Automatically prepend query constraints as pipeline stages complete_pipeline = [] lookup_stages = [] # Track if we have $inQuery constraints # Add $match stage from where constraints if any exist unless @where.empty? # `compile_where` is marker-free; `compile_markers` carries the # __aggregation_pipeline stages we need to extract below. where_clause = compile_where markers = compile_markers if where_clause.any? || markers.key?("__aggregation_pipeline") # Collect match conditions and stages initial_match_conditions = [] aggregation_match_conditions = [] non_match_stages = [] post_lookup_match = {} # `where_clause` is already marker-free; treat as regular constraints. regular_constraints = where_clause if regular_constraints.any? # Handle dates first date_converted = convert_dates_for_aggregation(regular_constraints) # Extract $inQuery/$notInQuery and convert to $lookup stages if has_subquery_constraints?(date_converted) lookup_result = extract_subquery_to_lookup_stages(date_converted) date_converted = lookup_result[:constraints] lookup_stages = lookup_result[:lookup_stages] post_lookup_match = lookup_result[:post_lookup_match] end # Convert field names for aggregation context and handle pointers if date_converted.any? match_stage = convert_constraints_for_aggregation(date_converted) initial_match_conditions << match_stage end end # Extract aggregation pipeline stages from the marker view. if markers.key?("__aggregation_pipeline") markers["__aggregation_pipeline"].each do |stage| if stage.is_a?(Hash) && stage.key?("$match") aggregation_match_conditions << stage["$match"] else non_match_stages << stage end end end # Stage 1: Initial $match with regular constraints if initial_match_conditions.any? if initial_match_conditions.length == 1 complete_pipeline << { "$match" => initial_match_conditions.first } else complete_pipeline << { "$match" => { "$and" => initial_match_conditions } } end end # Stage 2: $lookup stages for subqueries ($addFields, $lookup) if lookup_stages.any? lookup_stages.each do |stage| next if stage.key?("$project") complete_pipeline << stage end # Stage 3: Post-lookup $match if post_lookup_match.any? complete_pipeline << { "$match" => post_lookup_match } end # Note: Skip cleanup $project stage - see build_aggregation_pipeline for reasoning end # Stage 5: Aggregation $match conditions if aggregation_match_conditions.any? if aggregation_match_conditions.length == 1 complete_pipeline << { "$match" => aggregation_match_conditions.first } else complete_pipeline << { "$match" => { "$and" => aggregation_match_conditions } } end end # Stage 6: Non-$match stages from aggregation pipeline complete_pipeline.concat(non_match_stages) end end # Append the provided pipeline stages complete_pipeline.concat(pipeline) # Add $sort stage from order constraints if any exist unless @order.empty? sort_stage = {} @order.each do |order_obj| # order_obj is a Parse::Order object with field and direction field_name = order_obj.field.to_s direction = order_obj.direction == :desc ? -1 : 1 sort_stage[field_name] = direction end complete_pipeline << { "$sort" => sort_stage } if sort_stage.any? end # Add $skip stage if specified if @skip > 0 complete_pipeline << { "$skip" => @skip } end # Add $limit stage if specified if @limit.is_a?(Numeric) && @limit > 0 complete_pipeline << { "$limit" => @limit } end # Auto-detect if mongo_direct is needed (when $inQuery constraints are present and MongoDB is available) use_mongo_direct = mongo_direct if use_mongo_direct.nil? && lookup_stages && lookup_stages.any? && defined?(Parse::MongoDB) && Parse::MongoDB.enabled? use_mongo_direct = true end # Optimize pipeline by merging consecutive $match stages complete_pipeline = deduplicate_consecutive_match_stages(complete_pipeline) # When the pipeline is bound for direct MongoDB, translate every stage # through the direct-MongoDB field rewriter so user-supplied stages # (which use logical Parse field names like `$author`) reach the # correct on-disk columns (`$_p_author`). The Parse Server route does # not need this — Parse Server applies its own translation on the # aggregate endpoint — so the rewrite is gated on use_mongo_direct. if use_mongo_direct complete_pipeline = translate_pipeline_for_direct_mongodb(complete_pipeline) end Aggregation.new(self, complete_pipeline, verbose: verbose, mongo_direct: use_mongo_direct || false) end |
#aggregate_from_query(additional_stages = [], verbose: nil, mongo_direct: nil) ⇒ Aggregation
Converts the current query into an aggregate pipeline and executes it. This method automatically converts all query constraints (where, order, limit, skip, etc.) into MongoDB aggregation pipeline stages.
3201 3202 3203 3204 3205 3206 3207 3208 3209 3210 3211 3212 3213 3214 3215 3216 |
# File 'lib/parse/query.rb', line 3201 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.
3641 3642 3643 3644 3645 |
# File 'lib/parse/query.rb', line 3641 def all(expressions = { limit: :max }, &block) conditions(expressions) return results(&block) if block_given? results end |
#as_json(*args) ⇒ Hash
3767 3768 3769 |
# File 'lib/parse/query.rb', line 3767 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.
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 |
# File 'lib/parse/query.rb', line 2208 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.
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 |
# File 'lib/parse/query.rb', line 2264 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.
2099 2100 2101 2102 2103 2104 2105 2106 2107 2108 2109 2110 2111 2112 2113 2114 2115 2116 2117 2118 2119 2120 2121 2122 2123 2124 2125 2126 2127 2128 2129 2130 2131 2132 2133 2134 2135 2136 2137 2138 2139 2140 2141 2142 2143 2144 2145 2146 2147 2148 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 |
# File 'lib/parse/query.rb', line 2099 def atlas_search(query = nil, **, &block) require_relative "atlas_search" unless Parse::AtlasSearch.available? raise Parse::AtlasSearch::NotAvailable, "Atlas Search is not available. " \ "Call Parse::AtlasSearch.configure(enabled: true) after configuring Parse::MongoDB." end # Determine limit and skip from query or options limit = [:limit] || (@limit.is_a?(Numeric) && @limit > 0 ? @limit : 100) skip_val = [:skip] || (@skip > 0 ? @skip : 0) if block_given? # Builder block mode index_name = [:index] || Parse::AtlasSearch.default_index builder = Parse::AtlasSearch::SearchBuilder.new(index_name: index_name) yield builder # Build pipeline: $search must be first pipeline = [builder.build] # Add score projection pipeline << { "$addFields" => { "_score" => { "$meta" => "searchScore" } } } # Add existing query constraints as $match compiled_where = compile_where if compiled_where.present? regular_constraints = compiled_where.reject { |f, _| f == "__aggregation_pipeline" } if regular_constraints.any? mongo_constraints = convert_constraints_for_direct_mongodb(regular_constraints) pipeline << { "$match" => mongo_constraints } end end # Add sort, skip, limit pipeline << { "$sort" => { "_score" => -1 } } pipeline << { "$skip" => skip_val } if skip_val > 0 pipeline << { "$limit" => limit } # SDK-built pipeline only — see results_direct for rationale. raw_results = Parse::MongoDB.aggregate(@table, pipeline, allow_internal_fields: true, read_preference: @read_preference) # Convert results if [:raw] Parse::AtlasSearch::SearchResult.new(results: raw_results, raw_results: raw_results) else parse_results = Parse::MongoDB.convert_documents_to_parse(raw_results, @table) objects = parse_results.map { |doc| Parse.decode(doc) }.compact Parse::AtlasSearch::SearchResult.new(results: objects, raw_results: raw_results) end else # Simple options API - delegate to AtlasSearch module raise ArgumentError, "query string is required when not using a block" if query.nil? # Merge query constraints as filter compiled_where = compile_where if compiled_where.present? regular_constraints = compiled_where.reject { |f, _| f == "__aggregation_pipeline" } [:filter] = ([:filter] || {}).merge(regular_constraints) if regular_constraints.any? end [:class_name] = @table [:limit] = limit [:skip] = skip_val # Forward the query's read_preference (set via `#read_pref`). # Without this, Atlas Search calls reached through the Query # bridge silently fall back to the client default even though # the query explicitly opted in to a secondary read — the # mongo-direct path (`#results_direct`) honors it, this one # used to drop it on the floor. if @read_preference && !.key?(:read_preference) [:read_preference] = @read_preference end Parse::AtlasSearch.search(@table, query, **) end end |
#average(field) ⇒ Float Also known as: avg
Calculate the average of values for a specific field.
3888 3889 3890 3891 3892 3893 3894 3895 3896 3897 3898 3899 3900 3901 3902 |
# File 'lib/parse/query.rb', line 3888 def average(field) if field.nil? || !field.respond_to?(:to_s) raise ArgumentError, "Invalid field name passed to `average`." end # Format field name according to Parse conventions formatted_field = format_aggregation_field(field) # Build the aggregation pipeline pipeline = [ { "$group" => { "_id" => nil, "avg" => { "$avg" => "$#{formatted_field}" } } }, ] execute_basic_aggregation(pipeline, "average", field, "avg") end |
#before_prepare { ... } ⇒ Object
A callback called before the query is compiled
102 |
# File 'lib/parse/query.rb', line 102 define_model_callbacks :prepare, only: [:after, :before] |
#build_aggregation_pipeline ⇒ Array
Build the complete aggregation pipeline from constraints Pipeline order: $match (regular) -> $lookup (subqueries) -> $match (post-lookup) -> $match (aggregation) -> non-$match stages -> limit/skip
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 3489 3490 3491 3492 |
# File 'lib/parse/query.rb', line 3384 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).
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 2341 2342 2343 2344 2345 2346 2347 2348 2349 2350 2351 2352 2353 2354 2355 2356 2357 2358 2359 2360 2361 2362 2363 2364 2365 2366 2367 2368 2369 2370 2371 2372 2373 2374 2375 2376 2377 2378 2379 2380 2381 2382 2383 2384 2385 2386 2387 2388 2389 |
# File 'lib/parse/query.rb', line 2299 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
3593 3594 3595 3596 3597 3598 3599 3600 3601 3602 3603 3604 3605 3606 3607 3608 3609 3610 3611 |
# File 'lib/parse/query.rb', line 3593 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.
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 |
# File 'lib/parse/query.rb', line 2397 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
551 552 553 554 |
# File 'lib/parse/query.rb', line 551 def clause(clause_name = :where) return unless [:keys, :where, :order, :includes, :limit, :skip].include?(clause_name) instance_variable_get "@#{clause_name}".to_sym end |
#clear(item = :results) ⇒ self
Clear a specific clause of this query. This can be one of: :where, :order, :includes, :skip, :limit, :count, :keys or :results.
407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 |
# File 'lib/parse/query.rb', line 407 def clear(item = :results) case item when :where # an array of Parse::Constraint subclasses @where = [] when :order # an array of Parse::Order objects @order = [] when :includes @includes = [] when :skip @skip = 0 when :limit @limit = nil when :count @count = 0 when :keys @keys = [] end @results = nil self # chaining end |
#clone ⇒ Parse::Query
The @client and @results instance variables are intentionally NOT cloned. The cloned query will use the default client when executed.
Creates a deep copy of this query object, allowing independent modifications
5067 5068 5069 5070 5071 5072 5073 5074 5075 5076 5077 5078 5079 5080 5081 5082 5083 5084 5085 5086 5087 5088 5089 5090 5091 |
# File 'lib/parse/query.rb', line 5067 def clone cloned_query = Parse::Query.new(self.instance_variable_get(:@table)) # Note: :client is intentionally excluded - it contains non-serializable objects # (Redis connections, Faraday connections) and should be obtained lazily [:count, :where, :order, :keys, :includes, :limit, :skip, :cache, :use_master_key].each do |param| if instance_variable_defined?(:"@#{param}") value = instance_variable_get(:"@#{param}") if value.is_a?(Array) || value.is_a?(Hash) # Use Marshal for deep copy of complex constraint objects begin cloned_value = Marshal.load(Marshal.dump(value)) rescue => e # Fallback to shallow copy if Marshal fails puts "[Parse::Query.clone] Marshal failed for #{param}: #{e.}, falling back to dup" cloned_value = value.dup end else cloned_value = value end cloned_query.instance_variable_set(:"@#{param}", cloned_value) end end cloned_query.instance_variable_set(:@results, nil) cloned_query end |
#compile(encode: true, includeClassName: false) ⇒ Hash
Complies the query and runs all prepare callbacks.
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 |
# File 'lib/parse/query.rb', line 3785 def compile(encode: true, includeClassName: false) # Validate includes vs keys before compiling validate_includes_vs_keys # When a `keys` allowlist is set alongside `include`, the parent pointer # field must also be in `keys` or Parse Server strips it before expanding # the include. Auto-add the top-level segment of each include so partial # fetches don't silently drop included pointers. merge_includes_into_keys! run_callbacks :prepare do q = {} #query q[:limit] = @limit if @limit.is_a?(Numeric) && @limit > 0 q[:skip] = @skip if @skip > 0 q[:include] = @includes.join(",") unless @includes.empty? q[:keys] = @keys.join(",") unless @keys.empty? q[:order] = @order.join(",") unless @order.empty? unless @where.empty? q[:where] = Parse::Query.compile_where(@where) q[:where] = q[:where].to_json if encode end if @count && @count > 0 # if count is requested q[:limit] = 0 q[:count] = 1 end if includeClassName q[:className] = @table end q end end |
#compile_where ⇒ Hash
Returns a hash representing just the ‘where` clause of this query, with SDK-internal routing markers stripped.
3822 3823 3824 |
# File 'lib/parse/query.rb', line 3822 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.
472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 |
# File 'lib/parse/query.rb', line 472 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
847 848 849 |
# File 'lib/parse/query.rb', line 847 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.
2779 2780 2781 2782 2783 2784 2785 2786 2787 |
# File 'lib/parse/query.rb', line 2779 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.
2491 2492 2493 2494 2495 2496 2497 2498 2499 2500 2501 2502 2503 2504 2505 2506 2507 2508 2509 2510 2511 2512 2513 2514 2515 2516 2517 |
# File 'lib/parse/query.rb', line 2491 def convert_constraints_for_direct_mongodb(constraints) return constraints unless constraints.is_a?(Hash) result = {} constraints.each do |field, value| field_str = field.to_s # Skip special operators if field_str.start_with?("$") # Recursively convert nested constraints in $and, $or, $nor if value.is_a?(Array) && %w[$and $or $nor].include?(field_str) result[field_str] = value.map { |v| convert_constraints_for_direct_mongodb(v) } else result[field_str] = value end next end # Convert field name for MongoDB mongo_field = convert_field_for_direct_mongodb(field_str) # Convert value result[mongo_field] = convert_value_for_direct_mongodb(field_str, value) end result end |
#convert_field_for_direct_mongodb(field) ⇒ String
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Convert a field name for direct MongoDB access.
2523 2524 2525 2526 2527 2528 2529 2530 2531 2532 2533 2534 2535 2536 2537 2538 2539 2540 2541 2542 2543 2544 2545 2546 2547 2548 2549 2550 2551 2552 2553 2554 2555 2556 2557 2558 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 2587 2588 2589 2590 2591 2592 2593 2594 2595 2596 2597 2598 2599 2600 2601 2602 2603 2604 2605 2606 2607 |
# File 'lib/parse/query.rb', line 2523 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.
2763 2764 2765 2766 2767 2768 2769 2770 2771 |
# File 'lib/parse/query.rb', line 2763 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.
2707 2708 2709 2710 2711 2712 2713 2714 2715 2716 2717 2718 2719 2720 |
# File 'lib/parse/query.rb', line 2707 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.
2729 2730 2731 2732 2733 2734 2735 2736 2737 |
# File 'lib/parse/query.rb', line 2729 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.)
2794 2795 2796 2797 2798 2799 2800 2801 2802 2803 |
# File 'lib/parse/query.rb', line 2794 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.
2741 2742 2743 2744 2745 2746 2747 2748 2749 2750 |
# File 'lib/parse/query.rb', line 2741 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.
2674 2675 2676 2677 2678 2679 2680 2681 2682 2683 2684 2685 2686 2687 2688 2689 2690 2691 2692 2693 2694 2695 2696 2697 2698 2699 2700 |
# File 'lib/parse/query.rb', line 2674 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.
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 |
# File 'lib/parse/query.rb', line 2614 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.
1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 |
# File 'lib/parse/query.rb', line 1074 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.
1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 |
# File 'lib/parse/query.rb', line 1907 def count_direct(session_token: nil, master: nil, acl_user: nil, acl_role: nil) require_relative "mongodb" Parse::MongoDB.require_gem! unless Parse::MongoDB.available? raise Parse::MongoDB::NotEnabled, "Direct MongoDB queries are not enabled. " \ "Call Parse::MongoDB.configure(uri: 'mongodb://...', enabled: true) first." end # Build the aggregation pipeline for direct MongoDB execution pipeline = build_direct_mongodb_pipeline # Remove limit and skip for count (we want total count) pipeline = pipeline.reject { |stage| stage.key?("$limit") || stage.key?("$skip") } # Add count stage pipeline << { "$count" => "count" } # When no explicit auth kwargs are provided, derive them from the # query's own auth state — same fallback as results_direct. if session_token.nil? && master.nil? && acl_user.nil? && acl_role.nil? auth = mongo_direct_auth_kwargs session_token = auth[:session_token] master = auth[:master] acl_user = auth[:acl_user] acl_role = auth[:acl_role] end # SDK-built pipeline only — see results_direct for rationale. # ACL simulation runs inside Parse::MongoDB.aggregate when # session_token: or master: is supplied. raw_results = Parse::MongoDB.aggregate(@table, pipeline, allow_internal_fields: true, session_token: session_token, master: master, acl_user: acl_user, acl_role: acl_role, read_preference: @read_preference) # Extract count from result return 0 if raw_results.empty? raw_results.first["count"] || 0 end |
#count_distinct(field) ⇒ Integer
This feature requires MongoDB aggregation pipeline support in Parse Server.
Perform a count distinct query using MongoDB aggregation pipeline. This counts the number of distinct values for a given field.
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 |
# File 'lib/parse/query.rb', line 1132 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.
2872 2873 2874 |
# File 'lib/parse/query.rb', line 2872 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.
3650 3651 3652 3653 3654 3655 3656 3657 3658 3659 |
# File 'lib/parse/query.rb', line 3650 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.
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 |
# File 'lib/parse/query.rb', line 2944 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.
952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 |
# File 'lib/parse/query.rb', line 952 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.
1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034 2035 2036 2037 2038 2039 2040 2041 2042 2043 2044 2045 2046 |
# File 'lib/parse/query.rb', line 1970 def distinct_direct(field, return_pointers: false, order: nil, session_token: nil, master: nil, acl_user: nil, acl_role: nil) require_relative "mongodb" Parse::MongoDB.require_gem! unless Parse::MongoDB.available? raise Parse::MongoDB::NotEnabled, "Direct MongoDB queries are not enabled. " \ "Call Parse::MongoDB.configure(uri: 'mongodb://...', enabled: true) first." end if field.nil? || !field.respond_to?(:to_s) || field.is_a?(Hash) || field.is_a?(Array) raise ArgumentError, "Invalid field name passed to `distinct_direct`." end sort_dir = distinct_sort_direction(order) # Convert field name for direct MongoDB access mongo_field = convert_field_for_direct_mongodb(Query.format_field(field)) # Build the base pipeline with match constraints pipeline = [] # Add match stage from query constraints. `compile_where` already # strips `__`-prefixed routing markers, so the result is safe to # forward to MongoDB. compiled_where = compile_where if compiled_where.present? mongo_constraints = convert_constraints_for_direct_mongodb(compiled_where) pipeline << { "$match" => mongo_constraints } if mongo_constraints.any? end # Add group, optional sort, and project stages for distinct pipeline << { "$group" => { "_id" => "$#{mongo_field}" } } pipeline << { "$sort" => { "_id" => sort_dir } } if sort_dir pipeline << { "$project" => { "_id" => 0, "value" => "$_id" } } # SDK-built pipeline only — see results_direct for rationale. # Forward auth kwargs so Parse::MongoDB.aggregate runs the # three-layer ACL + CLP + protectedFields simulation for scoped # agents. Without this, distinct silently returns the unscoped # universe (CLP-1 enforcement asymmetry vs. #count / #results). # When no explicit auth kwargs are provided, derive from the # query's own auth state — same fallback as results_direct. if session_token.nil? && master.nil? && acl_user.nil? && acl_role.nil? auth = mongo_direct_auth_kwargs session_token = auth[:session_token] master = auth[:master] acl_user = auth[:acl_user] acl_role = auth[:acl_role] end raw_results = Parse::MongoDB.aggregate(@table, pipeline, allow_internal_fields: true, read_preference: @read_preference, session_token: session_token, master: master, acl_user: acl_user, acl_role: acl_role) # Extract values from results values = raw_results.map { |doc| doc["value"] }.compact # Handle pointer conversion if needed if return_pointers || field_is_pointer?(Query.format_field(field)) values = values.map do |value| if value.is_a?(String) && value.include?("$") # MongoDB pointer format: "ClassName$objectId" class_name, object_id = value.split("$", 2) Parse::Pointer.new(class_name, object_id) else value end end end values end |
#distinct_direct_pointers(field, order: nil, session_token: nil, master: nil, acl_user: nil, acl_role: nil) ⇒ Array
Convenience method for distinct_direct that always returns Parse::Pointer objects for pointer fields.
2053 2054 2055 2056 2057 2058 |
# File 'lib/parse/query.rb', line 2053 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.
4159 4160 4161 4162 4163 4164 4165 4166 |
# File 'lib/parse/query.rb', line 4159 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).
1045 1046 1047 |
# File 'lib/parse/query.rb', line 1045 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.
1529 1530 1531 1532 1533 1534 |
# File 'lib/parse/query.rb', line 1529 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
1170 1171 1172 1173 |
# File 'lib/parse/query.rb', line 1170 def each(&block) return results.enum_for(:each) unless block_given? # Sparkling magic! results.each(&block) end |
#execute_aggregation_pipeline ⇒ Aggregation
Execute an aggregation pipeline for queries with pipeline constraints
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 |
# File 'lib/parse/query.rb', line 3346 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.
2926 2927 2928 2929 2930 2931 2932 2933 2934 2935 |
# File 'lib/parse/query.rb', line 2926 def explain compiled_query = compile compiled_query[:explain] = true response = client.find_objects(@table, compiled_query.as_json, **_opts) if response.error? puts "[ParseQuery:Explain] #{response.error}" return {} end response.result end |
#extract_subquery_to_lookup_stages(constraints) ⇒ Hash
Extract $inQuery and $notInQuery constraints and build $lookup stages for them. This converts Parse subquery constraints into MongoDB $lookup stages that join with the related collection and filter based on the subquery conditions. Uses raw MongoDB field names (_p_field) and returns results via .raw aggregation.
3500 3501 3502 3503 3504 3505 3506 3507 3508 3509 3510 3511 3512 3513 3514 3515 3516 3517 3518 3519 3520 3521 3522 3523 3524 3525 3526 3527 3528 3529 3530 3531 3532 3533 3534 3535 3536 3537 3538 3539 3540 3541 3542 3543 3544 3545 3546 3547 3548 3549 3550 3551 3552 3553 3554 3555 3556 3557 3558 3559 3560 3561 3562 3563 3564 3565 3566 3567 3568 3569 3570 3571 3572 3573 3574 3575 3576 3577 3578 3579 3580 3581 3582 3583 3584 3585 3586 3587 3588 |
# File 'lib/parse/query.rb', line 3500 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.
1403 1404 1405 1406 1407 1408 1409 |
# File 'lib/parse/query.rb', line 1403 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.
1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 |
# File 'lib/parse/query.rb', line 1206 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.
1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 |
# File 'lib/parse/query.rb', line 1857 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.
1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 |
# File 'lib/parse/query.rb', line 1293 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.
2461 2462 2463 2464 2465 2466 2467 2468 2469 2470 2471 2472 2473 2474 2475 2476 2477 2478 2479 2480 2481 2482 2483 2484 2485 |
# File 'lib/parse/query.rb', line 2461 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.
3971 3972 3973 3974 3975 3976 3977 3978 3979 3980 3981 |
# File 'lib/parse/query.rb', line 3971 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.
4131 4132 4133 4134 4135 4136 4137 4138 4139 4140 4141 4142 4143 4144 4145 |
# File 'lib/parse/query.rb', line 4131 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.
4005 4006 4007 4008 4009 4010 4011 4012 4013 4014 4015 4016 4017 4018 4019 4020 4021 4022 4023 4024 4025 4026 4027 4028 4029 4030 4031 4032 4033 4034 4035 4036 4037 4038 4039 4040 4041 4042 4043 4044 4045 4046 4047 |
# File 'lib/parse/query.rb', line 4005 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
3616 3617 3618 3619 3620 3621 3622 3623 3624 3625 3626 3627 3628 3629 3630 |
# File 'lib/parse/query.rb', line 3616 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
792 793 794 |
# File 'lib/parse/query.rb', line 792 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.
779 780 781 782 783 784 785 786 787 788 789 |
# File 'lib/parse/query.rb', line 779 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.
570 571 572 573 574 575 576 577 578 579 580 |
# File 'lib/parse/query.rb', line 570 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).
1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 |
# File 'lib/parse/query.rb', line 1277 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).
1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 |
# File 'lib/parse/query.rb', line 1255 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`.
722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 |
# File 'lib/parse/query.rb', line 722 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
1178 1179 1180 1181 |
# File 'lib/parse/query.rb', line 1178 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.
3928 3929 3930 3931 3932 3933 3934 3935 3936 3937 3938 3939 3940 3941 3942 |
# File 'lib/parse/query.rb', line 3928 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.
3909 3910 3911 3912 3913 3914 3915 3916 3917 3918 3919 3920 3921 3922 3923 |
# File 'lib/parse/query.rb', line 3909 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 “*”.
5249 5250 5251 5252 5253 |
# File 'lib/parse/query.rb', line 5249 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 “*”.
5262 5263 5264 5265 5266 |
# File 'lib/parse/query.rb', line 5262 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.
897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 |
# File 'lib/parse/query.rb', line 897 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.
643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 |
# File 'lib/parse/query.rb', line 643 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
3838 3839 3840 3841 3842 3843 3844 3845 3846 3847 3848 3849 3850 3851 3852 |
# File 'lib/parse/query.rb', line 3838 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
3375 3376 3377 3378 3379 |
# File 'lib/parse/query.rb', line 3375 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.
601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 |
# File 'lib/parse/query.rb', line 601 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.
3775 3776 3777 |
# File 'lib/parse/query.rb', line 3775 def prepared(includeClassName: false) compile(encode: false, includeClassName: includeClassName) end |
#pretty ⇒ String
Retruns a formatted JSON string representing the query, useful for debugging.
3862 3863 3864 |
# File 'lib/parse/query.rb', line 3862 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.
5235 5236 5237 5238 |
# File 'lib/parse/query.rb', line 5235 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.
5207 5208 5209 |
# File 'lib/parse/query.rb', line 5207 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.
5221 5222 5223 |
# File 'lib/parse/query.rb', line 5221 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 “*”.
5183 5184 5185 |
# File 'lib/parse/query.rb', line 5183 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.
5195 5196 5197 |
# File 'lib/parse/query.rb', line 5195 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)
1743 1744 1745 |
# File 'lib/parse/query.rb', line 1743 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.
755 756 757 758 |
# File 'lib/parse/query.rb', line 755 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.
5111 5112 5113 5114 5115 |
# File 'lib/parse/query.rb', line 5111 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).
5126 5127 5128 5129 5130 |
# File 'lib/parse/query.rb', line 5126 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
760 761 762 763 764 |
# File 'lib/parse/query.rb', line 760 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
3856 3857 3858 |
# File 'lib/parse/query.rb', line 3856 def requires_aggregation? !pipeline.empty? end |
#requires_aggregation_pipeline? ⇒ Boolean
Check if this query contains constraints that require aggregation pipeline processing
1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 |
# File 'lib/parse/query.rb', line 1724 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.
1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 |
# File 'lib/parse/query.rb', line 1512 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
1751 1752 1753 |
# File 'lib/parse/query.rb', line 1751 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.
1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 |
# File 'lib/parse/query.rb', line 1436 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.
1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 |
# File 'lib/parse/query.rb', line 1780 def results_direct(raw: false, max_time_ms: nil, session_token: nil, master: nil, acl_user: nil, acl_role: nil, &block) require_relative "mongodb" Parse::MongoDB.require_gem! unless Parse::MongoDB.available? raise Parse::MongoDB::NotEnabled, "Direct MongoDB queries are not enabled. " \ "Call Parse::MongoDB.configure(uri: 'mongodb://...', enabled: true) first." end # Build the aggregation pipeline for direct MongoDB execution pipeline = build_direct_mongodb_pipeline # When no explicit auth kwargs are provided by the caller, derive them # from the query's own auth state (session_token, acl_user, acl_role, or # master key) via mongo_direct_auth_kwargs — exactly the same fallback # used by distinct_direct, count_direct, and the requires_mongo_direct? # auto-route in results(). Without this, a plain .results_direct call on # a master-key client would resolve as anonymous and have the ACL match # stage filter out every row whose _rperm is [] (the default for objects # created without an explicit public-read ACL). if session_token.nil? && master.nil? && acl_user.nil? && acl_role.nil? auth = mongo_direct_auth_kwargs session_token = auth[:session_token] master = auth[:master] acl_user = auth[:acl_user] acl_role = auth[:acl_role] end # Execute the aggregation directly on MongoDB. The pipeline was built # entirely from SDK constraint translation (no user-supplied stages), # so legitimate +_rperm+/+_wperm+ references emitted by # {#readable_by_role} and friends are sanctioned. The DENIED_OPERATORS # walk still runs at the MongoDB layer. When `session_token:` or # `master:` is supplied, Parse::MongoDB.aggregate adds the # three-layer ACL simulation (top-level $match, $lookup rewriter, # post-fetch redactor) before/after the pipeline executes. raw_results = Parse::MongoDB.aggregate(@table, pipeline, max_time_ms: max_time_ms, allow_internal_fields: true, session_token: session_token, master: master, acl_user: acl_user, acl_role: acl_role, read_preference: @read_preference) # Convert MongoDB documents to Parse format parse_results = Parse::MongoDB.convert_documents_to_parse(raw_results, @table) if raw return parse_results.each(&block) if block_given? return parse_results end # Convert to Parse objects items = decode(parse_results) return items.each(&block) if block_given? items end |
#rewrite_expression_for_direct_mongodb(expr) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Recursively rewrite field references inside an aggregation expression to their direct-MongoDB column names.
Walks Strings, Arrays, and Hashes:
-
A String starting with ‘$` (but not `$$`, which denotes a `let` variable or system variable like `$$ROOT`) is treated as a field reference. Its root path segment is rewritten via #convert_field_for_direct_mongodb, preserving any dot-delimited tail. Already-rewritten `$p*` references pass through unchanged.
-
Arrays and Hashes are recursed into, with one exception: the argument of ‘$literal` is a string constant, not a field reference, and must not be rewritten.
2820 2821 2822 2823 2824 2825 2826 2827 2828 2829 2830 2831 2832 2833 2834 2835 2836 2837 2838 2839 2840 2841 2842 2843 2844 |
# File 'lib/parse/query.rb', line 2820 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.
1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 |
# File 'lib/parse/query.rb', line 1608 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 `[{_rperm: {$exists: false}, {$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.
1573 1574 1575 1576 1577 1578 |
# File 'lib/parse/query.rb', line 1573 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
1186 1187 1188 1189 |
# File 'lib/parse/query.rb', line 1186 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.
684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 |
# File 'lib/parse/query.rb', line 684 def skip(amount) coerced = case amount when nil then 0 when Numeric then amount.to_i when String unless amount =~ /\A-?\d+\z/ raise ArgumentError, "Invalid skip #{amount.inspect}. Expected an Integer, " \ "a numeric String, or nil." end amount.to_i else raise ArgumentError, "Invalid skip #{amount.inspect}. Expected an Integer, " \ "a numeric String, or nil." end @skip = [0, coerced].max @results = nil self #chaining end |
#subscribe(fields: nil, session_token: nil, client: nil) ⇒ Parse::LiveQuery::Subscription
Subscribe to real-time updates for objects matching this query. Uses Parse LiveQuery WebSocket connection to receive push notifications when objects are created, updated, deleted, or enter/leave the query results.
2898 2899 2900 2901 2902 2903 2904 2905 2906 2907 2908 |
# File 'lib/parse/query.rb', line 2898 def subscribe(fields: nil, session_token: nil, client: nil) require_relative "live_query" lq_client = client || Parse::LiveQuery.client lq_client.subscribe( @table, where: compile_where, fields: fields, session_token: session_token || @session_token, ) end |
#sum(field) ⇒ Numeric
Calculate the sum of values for a specific field.
3869 3870 3871 3872 3873 3874 3875 3876 3877 3878 3879 3880 3881 3882 3883 |
# File 'lib/parse/query.rb', line 3869 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.
3729 3730 3731 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 |
# File 'lib/parse/query.rb', line 3729 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.
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 4084 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).
3148 3149 3150 3151 |
# File 'lib/parse/query.rb', line 3148 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.
3179 3180 3181 3182 3183 |
# File 'lib/parse/query.rb', line 3179 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.
3169 3170 3171 3172 3173 |
# File 'lib/parse/query.rb', line 3169 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.
872 873 874 875 876 877 878 879 |
# File 'lib/parse/query.rb', line 872 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.
854 855 856 |
# File 'lib/parse/query.rb', line 854 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.
5150 5151 5152 5153 5154 |
# File 'lib/parse/query.rb', line 5150 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).
5165 5166 5167 5168 5169 |
# File 'lib/parse/query.rb', line 5165 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.
927 928 929 930 931 932 |
# File 'lib/parse/query.rb', line 927 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 |