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}