Skip to main content

StorageAdapter

Trait StorageAdapter 

Source
pub trait StorageAdapter: Send + Sync {
Show 19 methods // Required methods fn all_schemas( &self, ) -> impl Future<Output = Result<Vec<ClassSchema>, ParseError>> + Send; fn upsert_schema( &self, schema: &ClassSchema, ) -> impl Future<Output = Result<(), ParseError>> + Send; fn insert_schema( &self, schema: &ClassSchema, ) -> impl Future<Output = Result<(), ParseError>> + Send; fn reserve_field( &self, class_name: &str, field_name: &str, field_type: &FieldType, options: Option<&ParseMap>, ) -> impl Future<Output = Result<AddFieldOutcome, ParseError>> + Send; fn set_field_options( &self, class_name: &str, field_name: &str, options: &ParseMap, ) -> impl Future<Output = Result<(), ParseError>> + Send; fn set_indexes( &self, class_name: &str, indexes: &ParseMap, ) -> impl Future<Output = Result<(), ParseError>> + Send; fn set_class_permissions( &self, class_name: &str, clp: Option<&ClassLevelPermissions>, ) -> impl Future<Output = Result<(), ParseError>> + Send; fn delete_class( &self, schema: &ClassSchema, ) -> impl Future<Output = Result<(), ParseError>> + Send; fn delete_fields( &self, schema: &ClassSchema, fields: &[String], ) -> impl Future<Output = Result<(), ParseError>> + Send; fn create( &self, schema: &ClassSchema, row: &Row, ) -> impl Future<Output = Result<WriteResult, ParseError>> + Send; fn upsert_one( &self, schema: &ClassSchema, query: &Query, row: &Row, ) -> impl Future<Output = Result<(), ParseError>> + Send; fn find( &self, schema: &ClassSchema, query: &Query, options: &QueryOptions, ) -> impl Future<Output = Result<Vec<Row>, ParseError>> + Send; fn count( &self, schema: &ClassSchema, query: &Query, ) -> impl Future<Output = Result<u64, ParseError>> + Send; fn update( &self, schema: &ClassSchema, query: &Query, update: &Update, ) -> impl Future<Output = Result<u64, ParseError>> + Send; fn update_one_returning( &self, schema: &ClassSchema, query: &Query, update: &Update, ) -> impl Future<Output = Result<Option<Row>, ParseError>> + Send; fn delete( &self, schema: &ClassSchema, query: &Query, ) -> impl Future<Output = Result<u64, ParseError>> + Send; fn ensure_index( &self, class_name: &str, fields: &[&str], name: Option<&str>, unique: bool, case_insensitive: bool, ) -> impl Future<Output = Result<(), ParseError>> + Send; fn create_indexes( &self, class_name: &str, indexes: &[SchemaIndex], ) -> impl Future<Output = Result<(), ParseError>> + Send; fn drop_index( &self, class_name: &str, name: &str, ) -> impl Future<Output = Result<(), ParseError>> + Send;
}
Expand description

Storage operations.

async fn in trait, so this is not object-safe. That is deliberate for now: the server holds one concrete adapter chosen at construction, and boxing every call to support a dyn we do not need would cost allocations on the hot path. If a deployment ever needs to swap adapters at runtime, add a boxed wrapper rather than degrading this.

Required Methods§

Source

fn all_schemas( &self, ) -> impl Future<Output = Result<Vec<ClassSchema>, ParseError>> + Send

Load every class schema. Upstream has no per-class fetch: a miss on any class triggers a full getAllClasses, and reproducing that shape keeps the caching behavior comparable.

Source

fn upsert_schema( &self, schema: &ClassSchema, ) -> impl Future<Output = Result<(), ParseError>> + Send

Persist a class schema, creating the class if it does not exist.

Must not clobber metadata it was not given. A field-adding write reaches here with clp: None simply because nothing loaded one, and rewriting the whole _metadata block from that would silently delete a class’s permissions on every ordinary save. The implementation sets the field keys it knows about and leaves _metadata alone unless the corresponding field on ClassSchema is Some.

Source

fn insert_schema( &self, schema: &ClassSchema, ) -> impl Future<Output = Result<(), ParseError>> + Send

Insert a class schema, failing if the class already exists.

The schema API’s create path, and it is an insert rather than an upsert for the same reason reserve_field is a conditional update. Upstream calls insertSchema, which is insertOne, and turns the backend’s duplicate-key error into DUPLICATE_VALUE Class already exists. (MongoSchemaCollection.js:183-195); the caller then re-labels it as INVALID_CLASS_NAME (SchemaController.js:861-864).

Reading the schema list and then upserting looks equivalent and is not. Two concurrent POST /schemas for one class both pass the read and both write, so both report success and the loser’s field types and CLP silently replace the winner’s. The class-already-exists answer has to come from the write, because only the write is atomic.

Returns DUPLICATE_VALUE with upstream’s message when the class exists.

Source

fn reserve_field( &self, class_name: &str, field_name: &str, field_type: &FieldType, options: Option<&ParseMap>, ) -> impl Future<Output = Result<AddFieldOutcome, ParseError>> + Send

Reserve a field type atomically, before any row is written, together with its options.

This is the fix for the concurrent first-write race 0.1.0 shipped with. Upstream issues a conditional upsert, {_id: class, field: {$exists: false}} / $set: {field: type} / upsert: true (MongoSchemaCollection.js:249-281), so a losing writer fails the condition rather than overwriting the winner’s type. Any backend that cannot express a conditional insert cannot implement Parse’s schema semantics safely, which is why this is on the trait rather than inside the Mongo adapter.

options is part of the same conditional update, not a second write. Upstream sets the type and _metadata.fields_options.<field> in one $set under the one $exists: false guard (MongoSchemaCollection.js:251-269). Splitting them lets a request reserve a type and then lose its options to a concurrent writer, which is the whole failure this method exists to prevent, one level down.

Source

fn set_field_options( &self, class_name: &str, field_name: &str, options: &ParseMap, ) -> impl Future<Output = Result<(), ParseError>> + Send

Set one field’s options, for a field that already exists.

updateFieldOptions (MongoSchemaCollection.js:284-300), reached when a submitted field matches the stored type and differs only in its options (SchemaController.js:1174-1180). Addressed per field, never as a block: the caller does not know what options its siblings carry and must not be able to erase them.

Source

fn set_indexes( &self, class_name: &str, indexes: &ParseMap, ) -> impl Future<Output = Result<(), ParseError>> + Send

Replace _metadata.indexes with the block the request produced.

The tail of setIndexesWithSchemaFormat (MongoStorageAdapter.js:404-408). Whole-block by design, unlike field options: the caller computed it by merging the submitted block into the stored one, which is the same read-modify-write upstream does.

Does not create the row. Upstream uses updateSchema, not upsertSchema, so on a class that does not exist yet this is a no-op and the indexes reach _SCHEMA through the insert instead.

Source

fn set_class_permissions( &self, class_name: &str, clp: Option<&ClassLevelPermissions>, ) -> impl Future<Output = Result<(), ParseError>> + Send

Replace _metadata.class_permissions. None removes the key, which is not the same as storing an empty block: see ClassSchema::clp.

Source

fn delete_class( &self, schema: &ClassSchema, ) -> impl Future<Output = Result<(), ParseError>> + Send

Drop a class: its rows, its schema entry and every join collection belonging to it.

Upstream refuses on a non-empty class at the REST layer (code 255), not here, so this does what it is told.

Source

fn delete_fields( &self, schema: &ClassSchema, fields: &[String], ) -> impl Future<Output = Result<(), ParseError>> + Send

Remove fields from a class: the schema entry and the column on every row.

Deliberately does not touch join collections, matching MongoStorageAdapter.js:495-501. Dropping a Relation field leaves its join collection in place, and a class recreated with the same field name inherits the old memberships. That is upstream behavior and a client can observe it.

Source

fn create( &self, schema: &ClassSchema, row: &Row, ) -> impl Future<Output = Result<WriteResult, ParseError>> + Send

Insert one row. object_id is generated by the caller, not the adapter, because it is part of the Parse contract rather than a storage detail.

Source

fn upsert_one( &self, schema: &ClassSchema, query: &Query, row: &Row, ) -> impl Future<Output = Result<(), ParseError>> + Send

Insert a row, or do nothing if one already matches.

Exists for join tables, whose membership rows carry no objectId and must be idempotent: adding a user to a role twice is one membership (DatabaseController.js:794-806).

Source

fn find( &self, schema: &ClassSchema, query: &Query, options: &QueryOptions, ) -> impl Future<Output = Result<Vec<Row>, ParseError>> + Send

Find rows matching the query.

Source

fn count( &self, schema: &ClassSchema, query: &Query, ) -> impl Future<Output = Result<u64, ParseError>> + Send

Count rows matching the query.

Source

fn update( &self, schema: &ClassSchema, query: &Query, update: &Update, ) -> impl Future<Output = Result<u64, ParseError>> + Send

Update matching rows.

Returns how many rows matched, so a caller can distinguish “updated nothing because the object does not exist” from “updated nothing because the ACL excluded it”. Upstream conflates those into OBJECT_NOT_FOUND, which is the behavior to reproduce at the REST layer, but the adapter should not throw the information away before then.

Source

fn update_one_returning( &self, schema: &ClassSchema, query: &Query, update: &Update, ) -> impl Future<Output = Result<Option<Row>, ParseError>> + Send

Update one row and return its post-image.

Needed because an update carrying an op has to tell the client the resulting value: _sanitizeDatabaseResult reads it off the document the adapter returns (DatabaseController.js:2129-2157), and upstream gets it from findOneAndUpdate with returnDocument: 'after' (MongoStorageAdapter.js:660-665). Ok(None) means nothing matched.

Source

fn delete( &self, schema: &ClassSchema, query: &Query, ) -> impl Future<Output = Result<u64, ParseError>> + Send

Delete matching rows. Returns how many, for the same reason as update.

Source

fn ensure_index( &self, class_name: &str, fields: &[&str], name: Option<&str>, unique: bool, case_insensitive: bool, ) -> impl Future<Output = Result<(), ParseError>> + Send

Create a unique index.

Index names are part of the contract. Both adapters recover duplicated_field by regex over the index name, and the Mongo regex matches only auto-generated <field>_1 names, so a differently-named index changes the error a client sees. name: None means “let the backend auto-name it”, which is what produces username_1. case_insensitive builds it under upstream’s collation, {locale: "en_US", strength: 2} (MongoCollection.js:134-136). Strength 2 ignores case and normalizes equivalent Unicode forms, and keeps diacritics significant, so Café and Cafe are different keys while a precomposed and a decomposed Café are one. unique is separate from case_insensitive and the two are not correlated. Upstream’s ensureIndex never sets unique at all (MongoStorageAdapter.js:782-812), so its case_insensitive_username is a plain collated index that exists to make the collated uniqueness query fast. Creating it unique instead is a mixed-fleet break rather than a stricter local choice: parse-server booting against the same database asks for the non-unique form under the same name and gets IndexKeySpecsConflict (86), so it refuses to start. Found exactly that way, by Gate D.

Source

fn create_indexes( &self, class_name: &str, indexes: &[SchemaIndex], ) -> impl Future<Output = Result<(), ParseError>> + Send

Create named indexes from the schema API’s indexes block.

Separate from StorageAdapter::ensure_index because these are not unique, are named by the caller rather than by the backend, and carry a caller-supplied key document including sort direction and _p_-prefixed pointer columns.

The write to _metadata.indexes is the caller’s, and it must not happen before this resolves (MongoStorageAdapter.js:398-408). A schema row claiming an index that was never built is worse than no index at all on a shared database: a parse-server node reading that row treats the index as present and will not create it either.

Source

fn drop_index( &self, class_name: &str, name: &str, ) -> impl Future<Output = Result<(), ParseError>> + Send

Drop an index by name, for the {"__op":"Delete"} form.

Dyn Compatibility§

This trait is not dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety".

Implementors§