Skip to main content

parse_rust_mongo/
adapter.rs

1//! The MongoDB `StorageAdapter`.
2
3use bson::{doc, Bson, Document};
4use futures::TryStreamExt;
5use mongodb::options::{IndexOptions, ReturnDocument};
6use mongodb::{Client, Database, IndexModel};
7use parse_rust_core::{
8    ClassLevelPermissions, ErrorCode, ParseError, ParseMap, ParseValue, DUPLICATE_VALUE_MESSAGE,
9};
10use parse_rust_schema::storage_format::{
11    field_type_to_storage, storage_to_field_type, NON_FIELD_KEYS,
12};
13use parse_rust_storage::{
14    join_table_name, AddFieldOutcome, ClassSchema, FieldType, Query, QueryOptions, Row,
15    SchemaIndex, SortDirection, StorageAdapter, Update, WriteResult,
16};
17
18use crate::transform::{
19    bson_document_to_parse_map, index_key_to_bson, mongo_object_to_parse,
20    parse_map_to_bson_document, parse_object_to_mongo_create, storage_key, transform_update,
21    transform_where,
22};
23
24/// Where class schemas live. Not configurable: parse-server hardcodes it, and a mixed fleet has
25/// to agree.
26const SCHEMA_COLLECTION: &str = "_SCHEMA";
27
28/// The three `_metadata` sub-keys parse-server writes.
29///
30/// **This list is closed.** A fourth key invented by parse-rust would travel into a database
31/// parse-server also reads, and `_metadata` is the one place a stray key is read back rather than
32/// rejected.
33const METADATA_CLASS_PERMISSIONS: &str = "_metadata.class_permissions";
34const METADATA_INDEXES: &str = "_metadata.indexes";
35const METADATA_FIELDS_OPTIONS: &str = "_metadata.fields_options";
36
37/// `emptyCLPS`, the merge base used when `class_permissions` **is** present
38/// (`MongoSchemaCollection.js:67-76`).
39///
40/// Note that this is not `defaultCLPS`. The two differ in more than their values: `defaultCLPS`
41/// carries an `ACL` key that this one does not, so a class whose CLP block is absent and a class
42/// whose CLP block sets only `find` do not read back as the same document.
43const EMPTY_CLPS_KEYS: [&str; 8] = [
44    "find",
45    "count",
46    "get",
47    "create",
48    "update",
49    "delete",
50    "addField",
51    "protectedFields",
52];
53
54pub struct MongoAdapter {
55    db: Database,
56}
57
58impl MongoAdapter {
59    pub async fn connect(uri: &str, database: &str) -> Result<Self, ParseError> {
60        let client = Client::with_uri_str(uri).await.map_err(mongo_err)?;
61        Ok(Self {
62            db: client.database(database),
63        })
64    }
65
66    /// Read the `_SCHEMA` document for one class.
67    async fn schema_document(&self, class_name: &str) -> Result<Option<Document>, ParseError> {
68        self.db
69            .collection::<Document>(SCHEMA_COLLECTION)
70            .find_one(doc! { "_id": class_name })
71            .await
72            .map_err(mongo_err)
73    }
74
75    /// Why a reservation matched nothing.
76    ///
77    /// Two conditions in the filter can refuse it and they need different answers, so the write
78    /// stays one operation and only the *explanation* costs a read. The read cannot change the
79    /// decision: it already happened.
80    ///
81    /// One place, deliberately: the driver reports "the upsert matched nothing" and "the upsert
82    /// collided on `_id`" differently, and both mean the same thing here, that somebody else
83    /// reserved the field first. Classifying in two places is how the two stop agreeing.
84    async fn reservation_refused(
85        &self,
86        class_name: &str,
87        field_name: &str,
88        requested: &FieldType,
89    ) -> Result<AddFieldOutcome, ParseError> {
90        if matches!(requested, FieldType::GeoPoint) {
91            let already = self
92                .schema_document(class_name)
93                .await?
94                .map(|d| {
95                    d.iter()
96                        .any(|(key, value)| key != field_name && value.as_str() == Some("geopoint"))
97                })
98                .unwrap_or(false);
99            if already {
100                // The adapter's own message, not `SchemaController`'s. Upstream raises this one
101                // from the Mongo schema collection (`MongoSchemaCollection.js:234`), and a client
102                // adding a second GeoPoint by an ordinary write sees it rather than the
103                // `validateObject` string. Measured against parse-server 9.10.1-alpha.6.
104                return Err(ParseError::incorrect_type(
105                    "MongoDB only supports one GeoPoint field in a class.".to_string(),
106                ));
107            }
108        }
109        self.classify_existing_field(class_name, field_name, requested)
110            .await
111    }
112
113    /// Decide the outcome of a field reservation by reading back what is actually stored.
114    async fn classify_existing_field(
115        &self,
116        class_name: &str,
117        field_name: &str,
118        requested: &FieldType,
119    ) -> Result<AddFieldOutcome, ParseError> {
120        let stored = self
121            .schema_document(class_name)
122            .await?
123            .and_then(|d| d.get_str(field_name).ok().map(str::to_string));
124
125        match stored.as_deref().and_then(storage_to_field_type) {
126            Some(existing) if &existing == requested => Ok(AddFieldOutcome::AlreadyPresentSameType),
127            Some(existing) => Ok(AddFieldOutcome::Conflict { existing }),
128            // Two cases fall here and neither can produce a `FieldType` to report:
129            //
130            // - the stored type string is one parse-server itself cannot read, because
131            //   `mongoFieldToParseSchemaField` is a `switch` with no default case
132            //   (`MongoSchemaCollection.js:4-39`) and the field's parsed entry becomes
133            //   `undefined`;
134            // - the field is absent on re-read, meaning it was deleted between the failed upsert
135            //   and this read.
136            //
137            // Upstream reaches the same place by a different route: `enforceFieldExists` swallows
138            // the failure, reloads, and `ensureFields` then finds no usable expected type and
139            // throws `INVALID_JSON` `Could not add field <name>` (`SchemaController.js:1205-1217`).
140            // Reporting that error is closer to upstream than inventing a `FieldType` to put in a
141            // `Conflict`, and it is loud rather than silent, which is what a caller needs when the
142            // reservation did not stick.
143            None => Err(ParseError::invalid_json(format!(
144                "Could not add field {field_name}"
145            ))),
146        }
147    }
148}
149
150/// Is this the duplicate-key error, whatever shape the driver wrapped it in?
151fn is_duplicate_key(e: &mongodb::error::Error) -> bool {
152    match e.kind.as_ref() {
153        mongodb::error::ErrorKind::Write(mongodb::error::WriteFailure::WriteError(we)) => {
154            we.code == 11000
155        }
156        mongodb::error::ErrorKind::Command(ce) => ce.code == 11000,
157        _ => false,
158    }
159}
160
161/// Dropping a collection that is not there is not a failure.
162///
163/// The modern driver returns success, but upstream still guards for the old `ns not found`
164/// (`MongoStorageAdapter.js:473-479`) and a class with no rows yet has no collection at all.
165fn is_namespace_not_found(e: &mongodb::error::Error) -> bool {
166    if let mongodb::error::ErrorKind::Command(ce) = e.kind.as_ref() {
167        return ce.code == 26;
168    }
169    false
170}
171
172/// Is this an infrastructure failure rather than a query-level one?
173///
174/// `isTransientError` (`MongoStorageAdapter.js:35-57`) names four driver error classes plus the
175/// `TransientTransactionError` label, and `handleError` turns them into a `Parse.Error` whose
176/// message is the fixed `Database error` (`MongoStorageAdapter.js:291-293`). The Rust driver
177/// splits the same territory differently, so this matches on the closest kinds rather than on
178/// upstream's error-name strings, which do not exist here.
179fn is_transient(e: &mongodb::error::Error) -> bool {
180    if e.contains_label(mongodb::error::TRANSIENT_TRANSACTION_ERROR) {
181        return true;
182    }
183    matches!(
184        e.kind.as_ref(),
185        mongodb::error::ErrorKind::ServerSelection { .. }
186            | mongodb::error::ErrorKind::ConnectionPoolCleared { .. }
187            | mongodb::error::ErrorKind::Io(_)
188    )
189}
190
191/// The server's own `errmsg`, which is where the index name lives.
192///
193/// Never put this on the wire. It reads
194/// `E11000 duplicate key error collection: <db>.<collection> index: <index> dup key: { <field>: <value> }`,
195/// so it names the database and the value that collided.
196fn driver_message(e: &mongodb::error::Error) -> Option<&str> {
197    match e.kind.as_ref() {
198        mongodb::error::ErrorKind::Write(mongodb::error::WriteFailure::WriteError(we)) => {
199            Some(we.message.as_str())
200        }
201        mongodb::error::ErrorKind::Command(ce) => Some(ce.message.as_str()),
202        _ => None,
203    }
204}
205
206/// The `_metadata.fields_options.<field>` path one field's options are addressed by.
207///
208/// A field name cannot contain a dot (`fieldNameIsValid`), so this cannot accidentally address
209/// something nested.
210fn field_options_path(field_name: &str) -> String {
211    format!("{METADATA_FIELDS_OPTIONS}.{field_name}")
212}
213
214/// The field columns a class schema writes: field name to `_SCHEMA` type string.
215fn schema_fields_document(schema: &ClassSchema) -> Document {
216    let mut out = Document::new();
217    for (name, ty) in &schema.fields {
218        let s = field_type_to_storage(ty);
219        // ACL renders empty and is never stored; writing it would be a phantom column.
220        if s.is_empty() {
221            continue;
222        }
223        out.insert(name.clone(), s);
224    }
225    out
226}
227
228/// The `_metadata` sub-document, **unprefixed**, and empty when the schema carries no metadata.
229///
230/// Kept separate from the field columns because the two writes need it in two different shapes and
231/// the shapes are not interchangeable. `upsert_schema` needs dotted paths, so that `$set`ting one
232/// metadata key leaves the others alone; `insert_schema` needs a real nested document, because a
233/// key containing a dot inside an inserted document is stored **literally**, as a top-level column
234/// named `_metadata.class_permissions`, which no reader on either server will ever look at.
235///
236/// Assembling one document for both is what this did for an hour, and the class it created had a
237/// CLP no reader could see, which reads exactly like a class with no CLP: default-open.
238fn schema_metadata_document(schema: &ClassSchema) -> Result<Document, ParseError> {
239    let mut out = Document::new();
240    if let Some(clp) = &schema.clp {
241        out.insert("class_permissions", parse_map_to_bson_document(clp.raw())?);
242    }
243    if let Some(indexes) = &schema.indexes {
244        out.insert("indexes", parse_map_to_bson_document(indexes)?);
245    }
246    if let Some(field_options) = &schema.field_options {
247        out.insert("fields_options", parse_map_to_bson_document(field_options)?);
248    }
249    Ok(out)
250}
251
252/// The index name out of a duplicate-key `errmsg`: the token after `index:`.
253fn index_name(message: &str) -> Option<&str> {
254    let (_, rest) = message.split_once(" index: ")?;
255    rest.split_whitespace().next()
256}
257
258/// Which field collided, or `None` when the index name does not say.
259///
260/// **Deliberately narrow.** Upstream reads only two index-name shapes: the auto-generated
261/// `<field>_1` (`MongoStorageAdapter.js:582`, which also accepts the legacy `<db>.$<field>_1`
262/// spelling) and the authData form `_auth_data_<provider>_id`
263/// (`MongoStorageAdapter.js:588`), tried only when the first found nothing. A custom index name
264/// yields no `duplicated_field` at all, which is why index names are contract: a differently
265/// named unique index on `username` changes the error a client sees. Parsing any index name here
266/// would hand `RestWrite`'s consumers a field upstream never gives them, and the two servers would
267/// then disagree about which error a client sees.
268///
269/// One difference from upstream's regex, and it is upstream's bug rather than a choice here.
270/// `/index:[\sa-zA-Z0-9_\-\.]+\$?([a-zA-Z_-]+)_1/` backtracks: against a modern MongoDB message
271/// (`index: username_1`) the greedy leading class eats all but the last letter, so the capture is
272/// `e`, not `username`. It captures correctly only against the legacy `index: <db>.$username_1`
273/// spelling, where `$` is outside the class and stops the greed. Upstream therefore reaches 202
274/// and 203 through the fallback queries at `RestWrite.js:1718-1755` rather than through
275/// `duplicated_field`, and the wire result is the same either way. This returns the whole field
276/// name, which is what the fast path was written to produce.
277fn duplicated_field(message: &str) -> Option<String> {
278    let name = index_name(message)?;
279    if let Some(prefix) = name.strip_suffix("_1") {
280        // The legacy spelling qualifies the index with the namespace: `<db>.$<field>_1`.
281        let field = prefix.rsplit(['$', '.']).next().unwrap_or(prefix);
282        // Upstream's capture class is `[a-zA-Z_-]`, so a field name containing a digit is not
283        // recoverable there and is not recoverable here either.
284        let recoverable = !field.is_empty()
285            && field
286                .chars()
287                .all(|c| c.is_ascii_alphabetic() || c == '_' || c == '-');
288        if recoverable {
289            return Some(field.to_string());
290        }
291    }
292    if name.starts_with("_auth_data_") && name.ends_with("_id") {
293        return Some(name.to_string());
294    }
295    None
296}
297
298fn mongo_err(e: mongodb::error::Error) -> ParseError {
299    // Duplicate key is the one storage error with a specific Parse code, because signup depends
300    // on it: `username_1` colliding must become 202, not a generic failure. The message is fixed
301    // and which field collided travels out of band, because the driver's text names the database
302    // and the colliding value (`MongoStorageAdapter.js:574-597`).
303    if is_duplicate_key(&e) {
304        let err = ParseError::new(ErrorCode::DuplicateValue, DUPLICATE_VALUE_MESSAGE);
305        return match driver_message(&e).and_then(duplicated_field) {
306            Some(field) => err.with_duplicated_field(field),
307            None => err,
308        };
309    }
310    if is_transient(&e) {
311        return ParseError::new(ErrorCode::InternalServerError, "Database error");
312    }
313    // Everything else is upstream's bare rethrow: a driver error that is not a `Parse.Error`, so
314    // the client gets the generic 500 body and this text reaches the log only.
315    ParseError::internal(format!("storage error: {e}"))
316}
317
318/// `{ ...emptyCLPS, ...stored }` (`MongoSchemaCollection.js:100`).
319///
320/// The merge is what makes an unspecified operation read back as `{}`, deny-all, rather than as
321/// absent, which is unrestricted. Handing the merged block to `ClassLevelPermissions::from_map`
322/// means `raw()` is the document that would be written back, so a round trip through parse-rust
323/// produces the block parse-server produces rather than the narrower one that was stored.
324fn merge_over_empty_clps(stored: &Document) -> Result<ParseMap, ParseError> {
325    let mut out = ParseMap::new();
326    for key in EMPTY_CLPS_KEYS {
327        out.insert(key.to_string(), ParseValue::Object(ParseMap::new()));
328    }
329    // `IndexMap::insert` keeps an existing key's position, which is what the JavaScript spread
330    // does, so the merged block has `emptyCLPS` order followed by any keys only the stored block
331    // carries.
332    for (key, value) in stored {
333        out.insert(key.clone(), crate::transform::bson_to_parse_value(value)?);
334    }
335    Ok(out)
336}
337
338impl StorageAdapter for MongoAdapter {
339    async fn all_schemas(&self) -> Result<Vec<ClassSchema>, ParseError> {
340        let mut cursor = self
341            .db
342            .collection::<Document>(SCHEMA_COLLECTION)
343            .find(doc! {})
344            .await
345            .map_err(mongo_err)?;
346
347        let mut out = Vec::new();
348        while let Some(doc) = cursor.try_next().await.map_err(mongo_err)? {
349            let Some(class_name) = doc.get_str("_id").ok() else {
350                continue;
351            };
352            let mut schema = parse_rust_schema::default_schema(class_name);
353            for (key, value) in &doc {
354                if NON_FIELD_KEYS.contains(&key.as_str()) {
355                    continue;
356                }
357                let Bson::String(type_str) = value else {
358                    continue;
359                };
360                // An unrecognised type string is skipped rather than erroring, which is what
361                // upstream's missing default case amounts to. A mixed fleet can contain one.
362                if let Some(ty) = storage_to_field_type(type_str) {
363                    schema.fields.insert(key.clone(), ty);
364                }
365            }
366
367            if let Ok(metadata) = doc.get_document("_metadata") {
368                // **Only when the key is present.** Absent stays `None`, and `None` is not
369                // "public": upstream reads an absent block back as `defaultCLPS`, a fully open
370                // document including an `ACL` key that the present-but-partial case never carries
371                // (`MongoSchemaCollection.js:95-101`). Materializing that default into the struct
372                // would make an absent block indistinguishable from an explicitly-public one, and
373                // the next write-back would then store a block parse-server never had, turning an
374                // unset CLP into a set one for every node reading the same database.
375                if let Ok(class_permissions) = metadata.get_document("class_permissions") {
376                    schema.clp = Some(ClassLevelPermissions::from_map(merge_over_empty_clps(
377                        class_permissions,
378                    )?));
379                }
380                // Round-tripped verbatim and never interpreted.
381                if let Ok(indexes) = metadata.get_document("indexes") {
382                    schema.indexes = Some(bson_document_to_parse_map(indexes)?);
383                }
384                if let Ok(fields_options) = metadata.get_document("fields_options") {
385                    schema.field_options = Some(bson_document_to_parse_map(fields_options)?);
386                }
387            }
388
389            out.push(schema);
390        }
391        Ok(out)
392    }
393
394    async fn insert_schema(&self, schema: &ClassSchema) -> Result<(), ParseError> {
395        let mut document = schema_fields_document(schema);
396        document.insert("_id", &schema.class_name);
397        // Nested, and omitted entirely when there is nothing in it, matching
398        // `mongoSchemaFromFieldsAndClassNameAndCLP`'s `delete mongoObject._metadata`
399        // (`MongoStorageAdapter.js:135-138`). An empty `_metadata` is a document parse-server
400        // never writes.
401        let metadata = schema_metadata_document(schema)?;
402        if !metadata.is_empty() {
403            document.insert("_metadata", metadata);
404        }
405        match self
406            .db
407            .collection::<Document>(SCHEMA_COLLECTION)
408            .insert_one(document)
409            .await
410        {
411            Ok(_) => Ok(()),
412            // `insertSchema`'s own catch, message included (`MongoSchemaCollection.js:188-190`).
413            // The caller re-labels it; this layer reports what the database said.
414            Err(e) if is_duplicate_key(&e) => Err(ParseError::new(
415                ErrorCode::DuplicateValue,
416                "Class already exists.",
417            )),
418            Err(e) => Err(mongo_err(e)),
419        }
420    }
421
422    async fn upsert_schema(&self, schema: &ClassSchema) -> Result<(), ParseError> {
423        let mut set = schema_fields_document(schema);
424        // Dotted paths, so that writing one metadata key leaves the other two alone. An ordinary
425        // field-adding save arrives with `clp: None` simply because nothing loaded one, and
426        // replacing `_metadata` wholesale from that would delete the class's permissions.
427        for (key, value) in schema_metadata_document(schema)? {
428            let path = match key.as_str() {
429                "class_permissions" => METADATA_CLASS_PERMISSIONS,
430                "indexes" => METADATA_INDEXES,
431                _ => METADATA_FIELDS_OPTIONS,
432            };
433            set.insert(path, value);
434        }
435
436        if set.is_empty() {
437            // Mongo rejects an empty update document, and `$set`ting `_id` to work around that
438            // would try to modify an immutable field. A class with nothing storable still has to
439            // have a row, so insert the bare one and treat an existing row as success.
440            let mut bare = Document::new();
441            bare.insert("_id", &schema.class_name);
442            return match self
443                .db
444                .collection::<Document>(SCHEMA_COLLECTION)
445                .insert_one(bare)
446                .await
447            {
448                Ok(_) => Ok(()),
449                Err(e) if is_duplicate_key(&e) => Ok(()),
450                Err(e) => Err(mongo_err(e)),
451            };
452        }
453
454        self.db
455            .collection::<Document>(SCHEMA_COLLECTION)
456            .update_one(doc! { "_id": &schema.class_name }, doc! { "$set": set })
457            .upsert(true)
458            .await
459            .map_err(mongo_err)?;
460        Ok(())
461    }
462
463    async fn reserve_field(
464        &self,
465        class_name: &str,
466        field_name: &str,
467        field_type: &FieldType,
468        options: Option<&ParseMap>,
469    ) -> Result<AddFieldOutcome, ParseError> {
470        let type_string = field_type_to_storage(field_type);
471        if type_string.is_empty() {
472            // `ACL` has no `_SCHEMA` string; it is injected on read. Reserving it would write an
473            // empty type string, which parse-server reads as a phantom field of unknown type.
474            return Err(ParseError::invalid_json(format!(
475                "Could not add field {field_name}"
476            )));
477        }
478
479        // The conditional upsert (`MongoSchemaCollection.js:249-281`, reaching
480        // `upsertSchema` at `:201-203`). The `$exists: false` guard is the whole mechanism: a
481        // writer that loses the race fails the condition rather than overwriting the winner's
482        // type, so the type a row is validated against cannot change under it.
483        let mut filter = Document::new();
484        filter.insert("_id", class_name);
485        filter.insert(field_name, doc! { "$exists": false });
486
487        let mut set = Document::new();
488        set.insert(field_name, &type_string);
489        // In the same `$set`, under the same guard (`MongoSchemaCollection.js:251-269`). A field
490        // reserved without its options would be a field whose options a concurrent writer can win.
491        if let Some(options) = options.filter(|o| !o.is_empty()) {
492            set.insert(
493                field_options_path(field_name),
494                parse_map_to_bson_document(options)?,
495            );
496        }
497
498        // **A class may hold only one GeoPoint field** (`MongoSchemaCollection.js:224-237`).
499        // Upstream reads the schema, checks it for an existing GeoPoint, then upserts, which is
500        // the read-decide-write split that two concurrent writers adding two *different* GeoPoint
501        // fields both survive. This was previously left to `parse-rust-schema` on the reasoning
502        // that it is a schema rule rather than a storage one; that is the wrong axis, because it
503        // is a predicate over stored state paired with a mutation of that state.
504        //
505        // It cannot go in the upsert filter. An upsert builds its insert document from the
506        // filter's equality terms, and `$expr` is not one, so Mongo refuses the whole operation and
507        // an ordinary first GeoPoint 500s. So the guarded form runs without `upsert`, and the class
508        // row is created by an explicit insert instead.
509        //
510        // **The insert is what closes the window, not a prior read.** Checking "does the class
511        // exist?" and then upserting is the same read-decide-write split this guard exists to
512        // remove: two writers can both observe absence, the first inserts GeoPoint `a`, and the
513        // second's plain `{b: {$exists: false}}` filter then adds `b` happily. `insert_one` is
514        // atomic on `_id`, so exactly one writer creates the row and the loser is told so by a
515        // duplicate-key error, at which point the guarded update is the right thing to retry.
516        //
517        // `$objectToArray` is what makes the condition expressible without a prior read. `_id` is
518        // a string and `_metadata` a subdocument, so neither can equal the type string.
519        if matches!(field_type, FieldType::GeoPoint) {
520            let mut guarded = filter.clone();
521            guarded.insert(
522                "$expr",
523                doc! { "$eq": [ { "$size": { "$filter": {
524                    "input": { "$objectToArray": "$$ROOT" },
525                    "cond": { "$eq": ["$$this.v", &type_string] },
526                } } }, 0 ] },
527            );
528            let guarded_update = || async {
529                self.db
530                    .collection::<Document>(SCHEMA_COLLECTION)
531                    .update_one(guarded.clone(), doc! { "$set": set.clone() })
532                    .await
533                    .map_err(mongo_err)
534            };
535
536            if guarded_update().await?.modified_count > 0 {
537                return Ok(AddFieldOutcome::Added);
538            }
539
540            // Matched nothing, so either the class row is absent or the guard refused. Try to
541            // create it. **Nested, not dotted**: a dotted key is a path inside `$set` and a
542            // literal key inside an inserted document, and writing `_metadata.fields_options.x`
543            // here would create a top-level column whose name contains dots.
544            let mut new_row = Document::new();
545            new_row.insert("_id", class_name);
546            new_row.insert(field_name, &type_string);
547            if let Some(options) = options.filter(|o| !o.is_empty()) {
548                new_row.insert(
549                    "_metadata",
550                    doc! { "fields_options": doc! {
551                        field_name: parse_map_to_bson_document(options)?,
552                    } },
553                );
554            }
555            return match self
556                .db
557                .collection::<Document>(SCHEMA_COLLECTION)
558                .insert_one(new_row)
559                .await
560            {
561                Ok(_) => Ok(AddFieldOutcome::Added),
562                // Somebody else created the class between the update and the insert. The row now
563                // exists, so the guarded update is meaningful again.
564                Err(e) if is_duplicate_key(&e) => {
565                    if guarded_update().await?.modified_count > 0 {
566                        return Ok(AddFieldOutcome::Added);
567                    }
568                    self.reservation_refused(class_name, field_name, field_type)
569                        .await
570                }
571                Err(e) => Err(mongo_err(e)),
572            };
573        }
574
575        let result = self
576            .db
577            .collection::<Document>(SCHEMA_COLLECTION)
578            .update_one(filter, doc! { "$set": set })
579            .upsert(true)
580            .await;
581
582        match result {
583            Ok(r) if r.upserted_id.is_some() || r.modified_count > 0 => Ok(AddFieldOutcome::Added),
584            // Matched nothing and did not error. The class document exists and the field does not,
585            // yet nothing changed, which a concurrent write can produce. Same classification as a
586            // collision.
587            Ok(_) => {
588                self.reservation_refused(class_name, field_name, field_type)
589                    .await
590            }
591            // The filter matched nothing because the field already exists, so the upsert tried to
592            // insert a second document with the same `_id` and collided.
593            Err(e) if is_duplicate_key(&e) => {
594                self.reservation_refused(class_name, field_name, field_type)
595                    .await
596            }
597            Err(e) => Err(mongo_err(e)),
598        }
599    }
600
601    async fn set_field_options(
602        &self,
603        class_name: &str,
604        field_name: &str,
605        options: &ParseMap,
606    ) -> Result<(), ParseError> {
607        // One path, one field, **under a `{field: {$exists: true}}` guard**
608        // (`MongoSchemaCollection.js:284-297`). Without it a field deleted between the caller's
609        // read and this write leaves options behind for a column that no longer exists, which
610        // parse-server reads back as a `fields_options` entry with no field.
611        //
612        // No upsert, unlike upstream. Upstream passes `upsert: true`, and because `$exists` is not
613        // an equality Mongo cannot derive the field from the filter, so the insert it attempts
614        // collides on `_id` and surfaces a raw duplicate-key error for what is really a lost race.
615        // Matching nothing and writing nothing is the same outcome without an error this layer
616        // would then have to sanitize. Recorded under the deliberate differences in `CHANGELOG.md`.
617        let mut filter = Document::new();
618        filter.insert("_id", class_name);
619        filter.insert(field_name, doc! { "$exists": true });
620        self.db
621            .collection::<Document>(SCHEMA_COLLECTION)
622            .update_one(
623                filter,
624                doc! { "$set": { field_options_path(field_name): parse_map_to_bson_document(options)? } },
625            )
626            .await
627            .map_err(mongo_err)?;
628        Ok(())
629    }
630
631    async fn set_indexes(&self, class_name: &str, indexes: &ParseMap) -> Result<(), ParseError> {
632        // No upsert, matching `updateSchema` (`MongoStorageAdapter.js:404-408`). On a class being
633        // created this matches nothing and the indexes travel in the insert instead.
634        self.db
635            .collection::<Document>(SCHEMA_COLLECTION)
636            .update_one(
637                doc! { "_id": class_name },
638                doc! { "$set": { METADATA_INDEXES: parse_map_to_bson_document(indexes)? } },
639            )
640            .await
641            .map_err(mongo_err)?;
642        Ok(())
643    }
644
645    async fn set_class_permissions(
646        &self,
647        class_name: &str,
648        clp: Option<&ClassLevelPermissions>,
649    ) -> Result<(), ParseError> {
650        // `$set` on the one path when there is a block, `$unset` when there is not
651        // (`MongoStorageAdapter.js:337-345` does the `$set` half). Removing the key is not the
652        // same as storing an empty block: the key's absence is what makes a class read back as
653        // `defaultCLPS`.
654        let update = match clp {
655            Some(clp) => {
656                doc! { "$set": { METADATA_CLASS_PERMISSIONS: parse_map_to_bson_document(clp.raw())? } }
657            }
658            None => doc! { "$unset": { METADATA_CLASS_PERMISSIONS: "" } },
659        };
660        // No upsert, matching `updateSchema`: setting permissions on a class that does not exist
661        // must not conjure one.
662        self.db
663            .collection::<Document>(SCHEMA_COLLECTION)
664            .update_one(doc! { "_id": class_name }, update)
665            .await
666            .map_err(mongo_err)?;
667        Ok(())
668    }
669
670    async fn delete_class(&self, schema: &ClassSchema) -> Result<(), ParseError> {
671        // Collected before the first await so the iterator does not borrow across it.
672        let joins: Vec<String> = schema
673            .relation_fields()
674            .map(|(field, _)| join_table_name(&schema.class_name, field))
675            .collect();
676
677        if let Err(e) = self
678            .db
679            .collection::<Document>(&schema.class_name)
680            .drop()
681            .await
682        {
683            if !is_namespace_not_found(&e) {
684                return Err(mongo_err(e));
685            }
686        }
687
688        self.db
689            .collection::<Document>(SCHEMA_COLLECTION)
690            .delete_one(doc! { "_id": &schema.class_name })
691            .await
692            .map_err(mongo_err)?;
693
694        // Every join collection belonging to the class goes with it
695        // (`DatabaseController.js:1631-1638`). Note that the join collections have no `_SCHEMA`
696        // row, so there is nothing else to remove for them.
697        for join in joins {
698            if let Err(e) = self.db.collection::<Document>(&join).drop().await {
699                if !is_namespace_not_found(&e) {
700                    return Err(mongo_err(e));
701                }
702            }
703        }
704        Ok(())
705    }
706
707    async fn delete_fields(
708        &self,
709        schema: &ClassSchema,
710        fields: &[String],
711    ) -> Result<(), ParseError> {
712        let mut column_unset = Document::new();
713        let mut existence: Vec<Bson> = Vec::new();
714        let mut schema_unset = Document::new();
715
716        for name in fields {
717            schema_unset.insert(name.clone(), Bson::Null);
718            schema_unset.insert(format!("{METADATA_FIELDS_OPTIONS}.{name}"), Bson::Null);
719
720            // A Relation has no column, so there is nothing to unset on the rows. Upstream issues
721            // the unset anyway and says so in a comment (`MongoStorageAdapter.js:495-499`); the
722            // difference is unobservable, because a stray column of that name is overwritten by
723            // the synthesized Relation on every read.
724            let column = match schema.field(name) {
725                Some(FieldType::Relation { .. }) => continue,
726                Some(FieldType::Pointer { .. }) => format!("_p_{name}"),
727                _ => name.clone(),
728            };
729            existence.push(Bson::Document(doc! { &column: { "$exists": true } }));
730            column_unset.insert(column, Bson::Null);
731        }
732
733        if !column_unset.is_empty() {
734            self.db
735                .collection::<Document>(&schema.class_name)
736                .update_many(doc! { "$or": existence }, doc! { "$unset": column_unset })
737                .await
738                .map_err(mongo_err)?;
739        }
740
741        if !schema_unset.is_empty() {
742            // **Deliberately does not touch join collections** (`MongoStorageAdapter.js:495-501`).
743            // Dropping a Relation field leaves its memberships in place, and a class recreated
744            // with the same field name inherits them. A client can observe that.
745            self.db
746                .collection::<Document>(SCHEMA_COLLECTION)
747                .update_one(
748                    doc! { "_id": &schema.class_name },
749                    doc! { "$unset": schema_unset },
750                )
751                .await
752                .map_err(mongo_err)?;
753        }
754        Ok(())
755    }
756
757    async fn create(&self, schema: &ClassSchema, row: &Row) -> Result<WriteResult, ParseError> {
758        let doc = parse_object_to_mongo_create(schema, row)?;
759        let object_id = doc
760            .get_str("_id")
761            .map_err(|_| ParseError::new(ErrorCode::MissingObjectId, "objectId is required"))?
762            .to_string();
763        self.db
764            .collection::<Document>(&schema.class_name)
765            .insert_one(doc)
766            .await
767            .map_err(mongo_err)?;
768        Ok(WriteResult { object_id })
769    }
770
771    async fn upsert_one(
772        &self,
773        schema: &ClassSchema,
774        query: &Query,
775        row: &Row,
776    ) -> Result<(), ParseError> {
777        // Upstream hands the same document to `transformUpdate`, which lifts plain values onto
778        // `$set` (`DatabaseController.js:794-806` calling `upsertOneObject`). A join membership is
779        // exactly `{relatedId, owningId}`, so adding a user to a role twice is one row.
780        //
781        // Nothing here writes to `_SCHEMA`. Join collections have no schema document upstream, and
782        // creating one would add a class every parse-server node reading the database would see.
783        let filter = transform_where(schema, query)?;
784        let set = parse_object_to_mongo_create(schema, row)?;
785        if set.is_empty() {
786            return Err(ParseError::invalid_json(
787                "upsert requires at least one value".to_string(),
788            ));
789        }
790        self.db
791            .collection::<Document>(&schema.class_name)
792            .update_one(filter, doc! { "$set": set })
793            .upsert(true)
794            .await
795            .map_err(mongo_err)?;
796        Ok(())
797    }
798
799    async fn find(
800        &self,
801        schema: &ClassSchema,
802        query: &Query,
803        options: &QueryOptions,
804    ) -> Result<Vec<Row>, ParseError> {
805        let filter = transform_where(schema, query)?;
806        // Bind the collection first: the driver's fluent builder borrows it, so building
807        // directly off a temporary would drop it while still in use.
808        let collection = self.db.collection::<Document>(&schema.class_name);
809        let mut find = collection.find(filter);
810
811        if options.case_insensitive {
812            // `{caseInsensitive: true}` resolves to this collation
813            // (`MongoStorageAdapter.js:801-803`, `MongoCollection.js:134-136`). Applied to the
814            // find rather than approximated in the filter, because strength 2 normalizes as well
815            // as folding case, which no regex over the stored value can reproduce.
816            find = find.collation(
817                mongodb::options::Collation::builder()
818                    .locale("en_US".to_string())
819                    .strength(mongodb::options::CollationStrength::Secondary)
820                    .build(),
821            );
822        }
823
824        if let Some(limit) = options.limit {
825            // The driver reads 0 as "no limit", which is the opposite of what a caller asking for
826            // zero rows means. Short-circuit instead.
827            if limit == 0 {
828                return Ok(Vec::new());
829            }
830            find = find.limit(limit as i64);
831        }
832        if let Some(skip) = options.skip {
833            find = find.skip(skip as u64);
834        }
835        if !options.order.is_empty() {
836            let mut sort = Document::new();
837            for (key, dir) in &options.order {
838                let dir = match dir {
839                    SortDirection::Ascending => 1,
840                    SortDirection::Descending => -1,
841                };
842                sort.insert(storage_key(schema, key), dir);
843            }
844            find = find.sort(sort);
845        }
846        if let Some(keys) = &options.keys {
847            let mut projection = Document::new();
848            for k in keys {
849                projection.insert(storage_key(schema, k), 1);
850            }
851            // Always projected regardless of `keys`:
852            //  - the permission columns, because ACL filtering happens after the read and
853            //    projecting them away would make every row look public;
854            //  - the timestamps, which Parse returns on every object whether asked for or not.
855            for always in ["_rperm", "_wperm", "_created_at", "_updated_at"] {
856                projection.insert(always, 1);
857            }
858            find = find.projection(projection);
859        }
860
861        let mut cursor = find.await.map_err(mongo_err)?;
862        let mut out = Vec::new();
863        while let Some(doc) = cursor.try_next().await.map_err(mongo_err)? {
864            out.push(mongo_object_to_parse(schema, &doc)?);
865        }
866        Ok(out)
867    }
868
869    async fn count(&self, schema: &ClassSchema, query: &Query) -> Result<u64, ParseError> {
870        let filter = transform_where(schema, query)?;
871        self.db
872            .collection::<Document>(&schema.class_name)
873            .count_documents(filter)
874            .await
875            .map_err(mongo_err)
876    }
877
878    async fn update(
879        &self,
880        schema: &ClassSchema,
881        query: &Query,
882        update: &Update,
883    ) -> Result<u64, ParseError> {
884        let filter = transform_where(schema, query)?;
885        let compiled = transform_update(schema, update)?;
886        // An update carrying nothing storable, a lone Relation for instance, compiles to an empty
887        // document, and Mongo rejects that. Nothing matched because nothing was asked for.
888        if compiled.is_empty() {
889            return Ok(0);
890        }
891        let res = self
892            .db
893            .collection::<Document>(&schema.class_name)
894            .update_many(filter, compiled)
895            .await
896            .map_err(mongo_err)?;
897        Ok(res.matched_count)
898    }
899
900    async fn update_one_returning(
901        &self,
902        schema: &ClassSchema,
903        query: &Query,
904        update: &Update,
905    ) -> Result<Option<Row>, ParseError> {
906        let filter = transform_where(schema, query)?;
907        let compiled = transform_update(schema, update)?;
908        if compiled.is_empty() {
909            return Ok(None);
910        }
911        // `returnDocument: 'after'` (`MongoStorageAdapter.js:660-665`). The post-image is what
912        // `_sanitizeDatabaseResult` reads an op's resulting value off, so the *before* image would
913        // report the old value and look like the op silently did nothing.
914        let collection = self.db.collection::<Document>(&schema.class_name);
915        let found = collection
916            .find_one_and_update(filter, compiled)
917            .return_document(ReturnDocument::After)
918            .await
919            .map_err(mongo_err)?;
920        found
921            .as_ref()
922            .map(|doc| mongo_object_to_parse(schema, doc))
923            .transpose()
924    }
925
926    async fn delete(&self, schema: &ClassSchema, query: &Query) -> Result<u64, ParseError> {
927        let filter = transform_where(schema, query)?;
928        let res = self
929            .db
930            .collection::<Document>(&schema.class_name)
931            .delete_many(filter)
932            .await
933            .map_err(mongo_err)?;
934        Ok(res.deleted_count)
935    }
936
937    async fn ensure_index(
938        &self,
939        class_name: &str,
940        fields: &[&str],
941        name: Option<&str>,
942        unique: bool,
943        case_insensitive: bool,
944    ) -> Result<(), ParseError> {
945        let mut keys = Document::new();
946        for f in fields {
947            keys.insert(f.to_string(), 1);
948        }
949        // Sparse and background, matching `ensureIndex`'s defaults
950        // (`MongoStorageAdapter.js:797`). A non-sparse unique index would refuse a second row
951        // with the field absent, which is not upstream's behavior.
952        let mut opts = IndexOptions::builder().unique(unique).sparse(true).build();
953        opts.name = name.map(str::to_string);
954        if case_insensitive {
955            // `caseInsensitiveCollation` (`MongoCollection.js:134-136`). Strength 2 ignores case
956            // and normalizes equivalent Unicode forms; it does **not** ignore diacritics, so
957            // `Café` and `Cafe` stay distinct. Normalization is the part a regex cannot reproduce.
958            opts.collation = Some(
959                mongodb::options::Collation::builder()
960                    .locale("en_US".to_string())
961                    .strength(mongodb::options::CollationStrength::Secondary)
962                    .build(),
963            );
964        }
965
966        self.db
967            .collection::<Document>(class_name)
968            .create_index(IndexModel::builder().keys(keys).options(opts).build())
969            .await
970            .map_err(mongo_err)?;
971        Ok(())
972    }
973
974    async fn create_indexes(
975        &self,
976        class_name: &str,
977        indexes: &[SchemaIndex],
978    ) -> Result<(), ParseError> {
979        if indexes.is_empty() {
980            return Ok(());
981        }
982        let mut models = Vec::with_capacity(indexes.len());
983        for index in indexes {
984            let mut keys = Document::new();
985            for (field, direction) in &index.keys {
986                // Passed through rather than coerced to `1`. `-1` is a descending key and
987                // `"text"`, `"hashed"` and `"2dsphere"` are index types, and all four are what a
988                // parse-server node reading `_metadata.indexes` will expect to find built.
989                keys.insert(
990                    field.clone(),
991                    index_key_to_bson(&index.name, field, direction)?,
992                );
993            }
994            let mut opts = IndexOptions::default();
995            // Named by the caller, never auto-generated. The name is the key `_metadata.indexes`
996            // is stored under and the handle `dropIndex` needs later.
997            opts.name = Some(index.name.clone());
998            models.push(IndexModel::builder().keys(keys).options(opts).build());
999        }
1000        self.db
1001            .collection::<Document>(class_name)
1002            .create_indexes(models)
1003            .await
1004            .map_err(mongo_err)?;
1005        Ok(())
1006    }
1007
1008    async fn drop_index(&self, class_name: &str, name: &str) -> Result<(), ParseError> {
1009        self.db
1010            .collection::<Document>(class_name)
1011            .drop_index(name)
1012            .await
1013            .map_err(mongo_err)?;
1014        Ok(())
1015    }
1016}
1017
1018#[cfg(test)]
1019mod tests {
1020    use super::*;
1021
1022    fn clp_doc() -> Document {
1023        doc! { "find": { "*": true }, "protectedFields": { "*": ["email"] } }
1024    }
1025
1026    /// The message the server actually sends, so the parser is tested against the real shape
1027    /// rather than against a convenient one.
1028    fn e11000(namespace: &str, index: &str, field: &str, value: &str) -> String {
1029        format!(
1030            "E11000 duplicate key error collection: {namespace} index: {index} dup key: {{ {field}: \"{value}\" }}"
1031        )
1032    }
1033
1034    #[test]
1035    fn the_auto_generated_index_name_yields_its_field() {
1036        for (index, field) in [
1037            ("username_1", "username"),
1038            ("email_1", "email"),
1039            ("name_1", "name"),
1040        ] {
1041            let message = e11000("appdb._User", index, field, "alice");
1042            assert_eq!(duplicated_field(&message).as_deref(), Some(field));
1043        }
1044    }
1045
1046    /// The pre-4.2 spelling qualifies the index with the namespace.
1047    #[test]
1048    fn the_legacy_namespace_qualified_index_name_yields_its_field() {
1049        let message =
1050            "E11000 duplicate key error index: appdb.$username_1 dup key: { : \"alice\" }";
1051        assert_eq!(duplicated_field(message).as_deref(), Some("username"));
1052    }
1053
1054    /// `_throwIfAuthDataDuplicate` reads this prefix, so the whole index name is the field.
1055    #[test]
1056    fn the_auth_data_index_name_is_carried_whole() {
1057        let message = e11000(
1058            "appdb._User",
1059            "_auth_data_facebook_id",
1060            "authData.facebook.id",
1061            "7",
1062        );
1063        assert_eq!(
1064            duplicated_field(&message).as_deref(),
1065            Some("_auth_data_facebook_id")
1066        );
1067    }
1068
1069    /// A name upstream cannot read must not be read here either. Recovering a field from
1070    /// `case_insensitive_username` would produce a `duplicated_field` upstream never produces,
1071    /// and the two servers would then answer a collision differently.
1072    #[test]
1073    fn an_index_name_outside_the_two_shapes_yields_nothing() {
1074        for index in [
1075            "case_insensitive_username",
1076            "username_2",
1077            "field1_1", // upstream's capture class excludes digits
1078            "_id_",
1079        ] {
1080            let message = e11000("appdb._User", index, "username", "alice");
1081            assert_eq!(duplicated_field(&message), None, "{index}");
1082        }
1083        assert_eq!(duplicated_field("some unrelated driver text"), None);
1084    }
1085
1086    /// The field name comes out; the database name and the colliding value do not.
1087    #[test]
1088    fn nothing_but_the_field_name_survives_the_parse() {
1089        let message = e11000("secret_prod_db._User", "username_1", "username", "alice");
1090        let field = duplicated_field(&message).expect("field");
1091        assert!(!field.contains("secret_prod_db"));
1092        assert!(!field.contains("alice"));
1093    }
1094
1095    /// The merge that makes an unspecified operation read back as deny-all rather than as absent.
1096    #[test]
1097    fn a_present_clp_block_merges_over_empty_clps() {
1098        let merged = merge_over_empty_clps(&clp_doc()).expect("merge");
1099
1100        // Every operation is present, and the ones the stored block did not mention are `{}`.
1101        for key in EMPTY_CLPS_KEYS {
1102            assert!(merged.contains_key(key), "{key} must be present");
1103        }
1104        assert!(
1105            matches!(merged.get("update"), Some(ParseValue::Object(m)) if m.is_empty()),
1106            "an unmentioned operation reads back as deny-all, not as absent"
1107        );
1108        assert!(
1109            matches!(merged.get("find"), Some(ParseValue::Object(m)) if m.contains_key("*")),
1110            "the stored value wins over the empty base"
1111        );
1112        // `defaultCLPS` carries an `ACL` key; `emptyCLPS` does not, and merging must not add one.
1113        assert!(
1114            !merged.contains_key("ACL"),
1115            "the present-but-partial case never carries an ACL key"
1116        );
1117    }
1118
1119    #[test]
1120    fn the_merged_block_keeps_empty_clps_key_order() {
1121        let merged =
1122            merge_over_empty_clps(&doc! { "delete": { "*": true }, "later": {} }).expect("merge");
1123        let keys: Vec<&str> = merged.keys().map(String::as_str).collect();
1124        assert_eq!(&keys[..8], &EMPTY_CLPS_KEYS);
1125        assert_eq!(
1126            keys[8], "later",
1127            "a key only the stored block has goes last"
1128        );
1129    }
1130
1131    /// The CLP block is what gets written back, so it has to survive a full round trip through
1132    /// BSON without losing a key parse-rust does not model.
1133    #[test]
1134    fn a_clp_block_round_trips_through_bson() {
1135        let mut stored = clp_doc();
1136        stored.insert("someFutureKey", doc! { "x": 1 });
1137        let merged = merge_over_empty_clps(&stored).expect("merge");
1138        let clp = ClassLevelPermissions::from_map(merged);
1139        let written = parse_map_to_bson_document(clp.raw()).expect("lower");
1140
1141        assert_eq!(
1142            written.get_document("someFutureKey").expect("kept"),
1143            &doc! { "x": 1 }
1144        );
1145        assert!(written.get_document("update").expect("update").is_empty());
1146    }
1147}