Module: Parse::Core::Indexing

Included in:
Object
Defined in:
lib/parse/model/core/indexing.rb

Overview

Model-declarative MongoDB index DSL. Mixed into Parse::Object so subclasses can declare the indexes they expect to exist on their collection. Declarations are inert at load time — they only land on MongoDB when Schema::IndexMigrator reads them and ‘apply_indexes!` is invoked through the writer connection.

SECURITY POSTURE — purely declarative. No network I/O, no class introspection that could leak data, no LLM-visible surface. The validation rules below run at declaration time so a typo surfaces as a load-time error, not a runtime surprise during ‘rake parse:mongo:indexes:apply` in prod.

Examples:

Declaring indexes

class Car < Parse::Object
  property :make, :string
  property :model, :string
  property :year, :integer
  property :tags, :array
  property :location, :geopoint
  belongs_to :owner, as: :user

  mongo_index :make, :model, :year                 # compound
  mongo_index :vin, unique: true
  mongo_index :owner                               # → _p_owner (pointer auto-rewrite)
  mongo_geo_index :location                        # 2dsphere
  mongo_index :tags                                # array column
  # mongo_index :tags, :categories                 # REJECTED: parallel arrays
end

Constant Summary collapse

MAX_INDEXES_PER_COLLECTION =

MongoDB limits each collection to 64 indexes total (including the implicit ‘id` index). The migrator’s plan phase reports remaining capacity using this constant.

64
PARSE_MANAGED_ARRAY_FIELDS =

Parse-managed array columns we can know about without introspecting actual data. Used by #assert_at_most_one_array_field! to catch parallel-array compounds at declaration time even when the parallel field is the ‘_rperm`/`_wperm` ACL array.

%w[_rperm _wperm].to_set.freeze
SENSITIVE_FIELDS =

Wire-format column names that hold Parse-internal secret material (password hashes, session tokens, verification tokens, auth provider blobs). The DSL refuses to declare an index on any of these because a queryable index on bcrypt hashes or session tokens turns ‘$indexStats` / collection-scan access into a credential-enumeration oracle. Parse Server already manages the legitimate indexes for these columns (see Schema::IndexMigrator::PARSE_MANAGED_INDEX_PATTERNS); this guard exists so a typo or malicious PR can’t add a new one.

%w[
  _hashed_password _session_token _email_verify_token
  _perishable_token _password_history authData _auth_data
].freeze

Instance Method Summary collapse

Instance Method Details

#apply_indexes!(drop: false) ⇒ Hash

Apply additive index changes via the writer connection. Pass ‘drop: true` to also drop orphan indexes; each drop carries its own audit log and confirmation envelope.

Returns:



175
176
177
# File 'lib/parse/model/core/indexing.rb', line 175

def apply_indexes!(drop: false)
  Parse::Schema::IndexMigrator.new(self).apply!(drop: drop)
end

#indexes_planHash{String=>Hash}

Dry-run reconciliation between declared indexes and what’s on the collection. Delegates to Schema::IndexMigrator.

Returns:

  • (Hash{String=>Hash})

    keyed by collection name; one entry per unique target collection across the declaration list (parent collection + any ‘_Join:*` collections from `mongo_relation_index`).



167
168
169
# File 'lib/parse/model/core/indexing.rb', line 167

def indexes_plan
  Parse::Schema::IndexMigrator.new(self).plan
end

#mongo_geo_index(field, sparse: false, name: nil) ⇒ Object

Sugar for a 2dsphere geospatial index. Geopoint columns are stored in Mongo as GeoJSON ‘{ type: “Point”, coordinates: [lng, lat] }` which `2dsphere` indexes natively.



95
96
97
98
# File 'lib/parse/model/core/indexing.rb', line 95

def mongo_geo_index(field, sparse: false, name: nil)
  register_index([field], key_value: "2dsphere", unique: false,
                 sparse: sparse, partial: nil, expire_after: nil, name: name)
end

#mongo_index(*fields, unique: false, sparse: false, partial: nil, expire_after: nil, name: nil) ⇒ Hash

Declare a regular (B-tree) index on one or more fields. Symbols in ‘fields` are looked up against the class’s ‘references` table — pointers auto-rewrite to `p<field>` so callers think in property names. Use `mongo_geo_index` for 2dsphere indexes.

Parameters:

  • fields (Array<Symbol>)

    property names; compound indexes are formed by passing more than one. Order matters for query prefix matching (MongoDB compound-index rule).

  • unique (Boolean) (defaults to: false)
  • sparse (Boolean) (defaults to: false)
  • partial (Hash, nil) (defaults to: nil)

    partial-index filter expression. Owner is responsible for lifecycle — Parse Server will not manage it.

  • expire_after (Integer, nil) (defaults to: nil)

    TTL in seconds (only valid on single-field indexes per MongoDB’s TTL rules).

  • name (String, nil) (defaults to: nil)

    explicit index name; defaults to ‘field_dir_field_dir` via MongoDB’s auto-naming.

Returns:

  • (Hash)

    the registered declaration (frozen)

Raises:

  • (ArgumentError)

    when validation rules fail (no fields, unknown field, parallel arrays, relation field, etc.)



86
87
88
89
90
# File 'lib/parse/model/core/indexing.rb', line 86

def mongo_index(*fields, unique: false, sparse: false, partial: nil,
                expire_after: nil, name: nil)
  register_index(fields, key_value: 1, unique: unique, sparse: sparse,
                 partial: partial, expire_after: expire_after, name: name)
end

#mongo_index_declarationsArray<Hash>

Storage for declared indexes. Each entry is a frozen Hash with the keys ‘:keys`, `:options`, `:declared_for` (the source-of-truth symbol list from the `mongo_index` call, for diagnostics).

Returns:



63
64
65
# File 'lib/parse/model/core/indexing.rb', line 63

def mongo_index_declarations
  @mongo_index_declarations ||= []
end

#mongo_relation_index(field, bidirectional: false, unique: false) ⇒ Array<Hash>

Declare an index on a Parse Relation’s join collection. Relations are stored in ‘_Join:<field>:<ParentClass>` collections — these have no Ruby model, so an `add_index :field` against the parent class would index the wrong collection. This method routes the declaration to the correct join-collection name, with the conventional column shape: `owningId` is the parent-side foreign key, `relatedId` is the related-side.

Default: single declaration on ‘1` — the forward lookup (“what’s related to this owner”), which is the dominant pattern for most Parse Relation queries.

‘bidirectional: true` adds a second declaration on `1` — the reverse lookup (“which owners contain this related object”). For high-traffic auth patterns like `Parse::Role.users`, the reverse direction is often the heavier-used index.

Uniqueness is NOT supported on ‘mongo_relation_index` — a unique single-direction index on a `has_many :through => :relation` field is semantically broken (it would say each owner can hold at most one related, contradicting `has_many`). If you want to enforce no-duplicate-pair membership, declare a compound unique index directly via `Parse::MongoDB.create_index` or a later extension to this DSL.

Examples:

Canonical case — role membership

class Parse::Role < Parse::Object
  has_many :users, through: :relation
  mongo_relation_index :users, bidirectional: true
  # creates: _Join:users:_Role { owningId: 1 }
  #         _Join:users:_Role { relatedId: 1 }
end

Parameters:

  • field (Symbol)

    the relation field name (must be declared via ‘has_many :field, through: :relation`)

  • bidirectional (Boolean) (defaults to: false)

    when true, register two declarations — one each for owningId and relatedId

Returns:

  • (Array<Hash>)

    the registered declarations

Raises:

  • (ArgumentError)

    when ‘field` is not a declared relation or `unique:` is passed (not supported on relation indexes)



141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/parse/model/core/indexing.rb', line 141

def mongo_relation_index(field, bidirectional: false, unique: false)
  if unique
    raise ArgumentError,
          "#{self}.mongo_relation_index does not support unique: — uniqueness on " \
          "a single-direction relation column breaks has_many semantics. For no-" \
          "duplicate-pair membership, declare a compound unique index directly " \
          "via Parse::MongoDB.create_index."
  end
  field = field.to_sym
  unless respond_to?(:relations) && relations.key?(field)
    raise ArgumentError,
          "#{self}.mongo_relation_index requires #{field.inspect} to be declared " \
          "via `has_many :#{field}, through: :relation`. Got non-relation field."
  end
  join_collection = "_Join:#{field}:#{parse_class}"
  decls = [register_relation_index(join_collection, "owningId", source: field)]
  decls << register_relation_index(join_collection, "relatedId", source: field) if bidirectional
  decls
end