Skip to main content

parse_rust_storage/
adapter.rs

1//! The `StorageAdapter` trait.
2//!
3//! **Shaped by two consumers, not one.** Building against Mongo alone bakes Mongo-isms into the
4//! interface and the Postgres port then fights it, which is what produced the 55 catalogued
5//! divergences upstream. The rule: if a method can only be implemented sensibly
6//! for one backend, the trait is wrong.
7//!
8//! Three consequences visible in the signatures below:
9//!
10//! - **No `$` operators and no BSON.** Queries are an AST the adapter lowers. Postgres cannot
11//!   lower a raw Mongo query document, so accepting one here would be the first Mongo-ism.
12//! - **Schema is passed in, not fetched.** The adapter does not own a schema cache. A caller that
13//!   already resolved the schema for a request threads it down, which is what keeps one request
14//!   from evaluating half its work under two different schemas.
15//! - **Updates are an op AST, not a row.** `Increment` is not a value; expressing it as one is
16//!   how 0.1.0 came to store `{"__op":"Increment"}` as a literal object.
17
18use std::future::Future;
19
20use parse_rust_core::{ClassLevelPermissions, ParseError, ParseMap, ParseValue};
21
22use crate::query::{Query, QueryOptions, Update};
23use crate::schema::{ClassSchema, FieldType};
24
25/// A row as stored: Parse-format values, no backend encoding.
26pub type Row = ParseMap;
27
28/// What a write returns.
29///
30/// Deliberately not the full row. Upstream's create response is `{objectId, createdAt}` and its
31/// update response is `{updatedAt}`, and returning more here would tempt a caller into sending
32/// more than parse-server does.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct WriteResult {
35    pub object_id: String,
36}
37
38/// What happened when a field type was reserved.
39///
40/// This replaces upstream's error-code sniffing. `enforceFieldExists` calls
41/// `addFieldIfNotExists`, swallows every error that is not `INCORRECT_TYPE`, reloads the schema
42/// and re-validates (`SchemaController.js:1184-1216`), which means "another writer won the race
43/// with the same type" and "another writer won the race with a different type" are distinguished
44/// only by what a *second* read finds. Making that an enum means a caller cannot conflate them.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub enum AddFieldOutcome {
47    /// This caller reserved the type.
48    Added,
49    /// Someone else got there first, with the same type. Not an error: concurrent writers
50    /// inferring the same type both succeed upstream.
51    AlreadyPresentSameType,
52    /// Someone else got there first with an incompatible type. The caller's write must fail.
53    Conflict { existing: FieldType },
54}
55
56/// Storage operations.
57///
58/// `async fn` in trait, so this is not object-safe. That is deliberate for now: the server holds
59/// one concrete adapter chosen at construction, and boxing every call to support a `dyn` we do
60/// not need would cost allocations on the hot path. If a deployment ever needs to swap adapters
61/// at runtime, add a boxed wrapper rather than degrading this.
62pub trait StorageAdapter: Send + Sync {
63    /// Load every class schema. Upstream has no per-class fetch: a miss on any class triggers a
64    /// full `getAllClasses`, and reproducing that shape keeps the caching behavior comparable.
65    fn all_schemas(&self) -> impl Future<Output = Result<Vec<ClassSchema>, ParseError>> + Send;
66
67    /// Persist a class schema, creating the class if it does not exist.
68    ///
69    /// **Must not clobber metadata it was not given.** A field-adding write reaches here with
70    /// `clp: None` simply because nothing loaded one, and rewriting the whole `_metadata` block
71    /// from that would silently delete a class's permissions on every ordinary save. The
72    /// implementation sets the field keys it knows about and leaves `_metadata` alone unless the
73    /// corresponding field on [`ClassSchema`] is `Some`.
74    fn upsert_schema(
75        &self,
76        schema: &ClassSchema,
77    ) -> impl Future<Output = Result<(), ParseError>> + Send;
78
79    /// Insert a class schema, failing if the class already exists.
80    ///
81    /// **The schema API's create path, and it is an insert rather than an upsert for the same
82    /// reason `reserve_field` is a conditional update.** Upstream calls `insertSchema`, which is
83    /// `insertOne`, and turns the backend's duplicate-key error into `DUPLICATE_VALUE`
84    /// `Class already exists.` (`MongoSchemaCollection.js:183-195`); the caller then re-labels it
85    /// as `INVALID_CLASS_NAME` (`SchemaController.js:861-864`).
86    ///
87    /// Reading the schema list and then upserting looks equivalent and is not. Two concurrent
88    /// `POST /schemas` for one class both pass the read and both write, so both report success and
89    /// the loser's field types and CLP silently replace the winner's. The class-already-exists
90    /// answer has to come from the write, because only the write is atomic.
91    ///
92    /// Returns `DUPLICATE_VALUE` with upstream's message when the class exists.
93    fn insert_schema(
94        &self,
95        schema: &ClassSchema,
96    ) -> impl Future<Output = Result<(), ParseError>> + Send;
97
98    /// Reserve a field type atomically, before any row is written, **together with its options**.
99    ///
100    /// This is the fix for the concurrent first-write race 0.1.0 shipped with. Upstream issues a
101    /// conditional upsert, `{_id: class, field: {$exists: false}}` / `$set: {field: type}` /
102    /// `upsert: true` (`MongoSchemaCollection.js:249-281`), so a losing writer fails the condition
103    /// rather than overwriting the winner's type. Any backend that cannot express a conditional
104    /// insert cannot implement Parse's schema semantics safely, which is why this is on the trait
105    /// rather than inside the Mongo adapter.
106    ///
107    /// **`options` is part of the same conditional update, not a second write.** Upstream sets the
108    /// type and `_metadata.fields_options.<field>` in one `$set` under the one `$exists: false`
109    /// guard (`MongoSchemaCollection.js:251-269`). Splitting them lets a request reserve a type and
110    /// then lose its options to a concurrent writer, which is the whole failure this method exists
111    /// to prevent, one level down.
112    fn reserve_field(
113        &self,
114        class_name: &str,
115        field_name: &str,
116        field_type: &FieldType,
117        options: Option<&ParseMap>,
118    ) -> impl Future<Output = Result<AddFieldOutcome, ParseError>> + Send;
119
120    /// Set one field's options, for a field that already exists.
121    ///
122    /// `updateFieldOptions` (`MongoSchemaCollection.js:284-300`), reached when a submitted field
123    /// matches the stored type and differs only in its options
124    /// (`SchemaController.js:1174-1180`). Addressed **per field**, never as a block: the caller
125    /// does not know what options its siblings carry and must not be able to erase them.
126    fn set_field_options(
127        &self,
128        class_name: &str,
129        field_name: &str,
130        options: &ParseMap,
131    ) -> impl Future<Output = Result<(), ParseError>> + Send;
132
133    /// Replace `_metadata.indexes` with the block the request produced.
134    ///
135    /// The tail of `setIndexesWithSchemaFormat` (`MongoStorageAdapter.js:404-408`). Whole-block by
136    /// design, unlike field options: the caller computed it by merging the submitted block into the
137    /// stored one, which is the same read-modify-write upstream does.
138    ///
139    /// **Does not create the row.** Upstream uses `updateSchema`, not `upsertSchema`, so on a class
140    /// that does not exist yet this is a no-op and the indexes reach `_SCHEMA` through the insert
141    /// instead.
142    fn set_indexes(
143        &self,
144        class_name: &str,
145        indexes: &ParseMap,
146    ) -> impl Future<Output = Result<(), ParseError>> + Send;
147
148    /// Replace `_metadata.class_permissions`. `None` removes the key, which is not the same as
149    /// storing an empty block: see [`ClassSchema::clp`].
150    fn set_class_permissions(
151        &self,
152        class_name: &str,
153        clp: Option<&ClassLevelPermissions>,
154    ) -> impl Future<Output = Result<(), ParseError>> + Send;
155
156    /// Drop a class: its rows, its schema entry and every join collection belonging to it.
157    ///
158    /// Upstream refuses on a non-empty class at the REST layer (code 255), not here, so this does
159    /// what it is told.
160    fn delete_class(
161        &self,
162        schema: &ClassSchema,
163    ) -> impl Future<Output = Result<(), ParseError>> + Send;
164
165    /// Remove fields from a class: the schema entry and the column on every row.
166    ///
167    /// Deliberately does **not** touch join collections, matching
168    /// `MongoStorageAdapter.js:495-501`. Dropping a `Relation` field leaves its join collection
169    /// in place, and a class recreated with the same field name inherits the old memberships.
170    /// That is upstream behavior and a client can observe it.
171    fn delete_fields(
172        &self,
173        schema: &ClassSchema,
174        fields: &[String],
175    ) -> impl Future<Output = Result<(), ParseError>> + Send;
176
177    /// Insert one row. `object_id` is generated by the caller, not the adapter, because it is
178    /// part of the Parse contract rather than a storage detail.
179    fn create(
180        &self,
181        schema: &ClassSchema,
182        row: &Row,
183    ) -> impl Future<Output = Result<WriteResult, ParseError>> + Send;
184
185    /// Insert a row, or do nothing if one already matches.
186    ///
187    /// Exists for join tables, whose membership rows carry no objectId and must be idempotent:
188    /// adding a user to a role twice is one membership (`DatabaseController.js:794-806`).
189    fn upsert_one(
190        &self,
191        schema: &ClassSchema,
192        query: &Query,
193        row: &Row,
194    ) -> impl Future<Output = Result<(), ParseError>> + Send;
195
196    /// Find rows matching the query.
197    fn find(
198        &self,
199        schema: &ClassSchema,
200        query: &Query,
201        options: &QueryOptions,
202    ) -> impl Future<Output = Result<Vec<Row>, ParseError>> + Send;
203
204    /// Count rows matching the query.
205    fn count(
206        &self,
207        schema: &ClassSchema,
208        query: &Query,
209    ) -> impl Future<Output = Result<u64, ParseError>> + Send;
210
211    /// Update matching rows.
212    ///
213    /// Returns how many rows matched, so a caller can distinguish "updated nothing because the
214    /// object does not exist" from "updated nothing because the ACL excluded it". Upstream
215    /// conflates those into `OBJECT_NOT_FOUND`, which is the behavior to reproduce at the REST
216    /// layer, but the adapter should not throw the information away before then.
217    fn update(
218        &self,
219        schema: &ClassSchema,
220        query: &Query,
221        update: &Update,
222    ) -> impl Future<Output = Result<u64, ParseError>> + Send;
223
224    /// Update one row and return its post-image.
225    ///
226    /// Needed because an update carrying an op has to tell the client the resulting value:
227    /// `_sanitizeDatabaseResult` reads it off the document the adapter returns
228    /// (`DatabaseController.js:2129-2157`), and upstream gets it from `findOneAndUpdate` with
229    /// `returnDocument: 'after'` (`MongoStorageAdapter.js:660-665`). `Ok(None)` means nothing
230    /// matched.
231    fn update_one_returning(
232        &self,
233        schema: &ClassSchema,
234        query: &Query,
235        update: &Update,
236    ) -> impl Future<Output = Result<Option<Row>, ParseError>> + Send;
237
238    /// Delete matching rows. Returns how many, for the same reason as `update`.
239    fn delete(
240        &self,
241        schema: &ClassSchema,
242        query: &Query,
243    ) -> impl Future<Output = Result<u64, ParseError>> + Send;
244
245    /// Create a unique index.
246    ///
247    /// **Index names are part of the contract.** Both adapters recover `duplicated_field` by regex
248    /// over the index name, and the Mongo regex matches only auto-generated `<field>_1` names, so
249    /// a differently-named index changes the error a client sees. `name: None` means "let the
250    /// backend auto-name it", which is what produces `username_1`.
251    /// `case_insensitive` builds it under upstream's collation, `{locale: "en_US", strength: 2}`
252    /// (`MongoCollection.js:134-136`). Strength 2 ignores case and normalizes equivalent Unicode
253    /// forms, and keeps diacritics significant, so `Café` and `Cafe` are different keys while a
254    /// precomposed and a decomposed `Café` are one.
255    /// `unique` is separate from `case_insensitive` and the two are not correlated. Upstream's
256    /// `ensureIndex` never sets `unique` at all (`MongoStorageAdapter.js:782-812`), so its
257    /// `case_insensitive_username` is a plain collated index that exists to make the collated
258    /// uniqueness *query* fast. Creating it unique instead is a mixed-fleet break rather than a
259    /// stricter local choice: parse-server booting against the same database asks for the
260    /// non-unique form under the same name and gets `IndexKeySpecsConflict` (86), so it refuses to
261    /// start. Found exactly that way, by Gate D.
262    fn ensure_index(
263        &self,
264        class_name: &str,
265        fields: &[&str],
266        name: Option<&str>,
267        unique: bool,
268        case_insensitive: bool,
269    ) -> impl Future<Output = Result<(), ParseError>> + Send;
270
271    /// Create named indexes from the schema API's `indexes` block.
272    ///
273    /// Separate from [`StorageAdapter::ensure_index`] because these are not unique, are
274    /// named by the caller rather than by the backend, and carry a caller-supplied key document
275    /// including sort direction and `_p_`-prefixed pointer columns.
276    ///
277    /// **The write to `_metadata.indexes` is the caller's, and it must not happen before this
278    /// resolves** (`MongoStorageAdapter.js:398-408`). A schema row claiming an index that was
279    /// never built is worse than no index at all on a shared database: a parse-server node reading
280    /// that row treats the index as present and will not create it either.
281    fn create_indexes(
282        &self,
283        class_name: &str,
284        indexes: &[SchemaIndex],
285    ) -> impl Future<Output = Result<(), ParseError>> + Send;
286
287    /// Drop an index by name, for the `{"__op":"Delete"}` form.
288    fn drop_index(
289        &self,
290        class_name: &str,
291        name: &str,
292    ) -> impl Future<Output = Result<(), ParseError>> + Send;
293}
294
295/// One entry of the schema API's `indexes` block: a name, and the key document under it.
296///
297/// The value of each key is passed through rather than normalized. Mongo reads `1` and `-1` as
298/// ascending and descending, and `"text"`, `"2dsphere"` and `"hashed"` as index types, and which
299/// of those a deployment used is recorded in `_SCHEMA` for every other node to read.
300#[derive(Debug, Clone)]
301pub struct SchemaIndex {
302    pub name: String,
303    pub keys: Vec<(String, ParseValue)>,
304}