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//! Two 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
16use std::future::Future;
17
18use parse_rust_core::{ParseError, ParseMap};
19
20use crate::query::{Constraint, QueryOptions};
21use crate::schema::ClassSchema;
22
23/// A row as stored: Parse-format values, no backend encoding.
24pub type Row = ParseMap;
25
26/// What a write returns.
27///
28/// Deliberately not the full row. Upstream's create response is `{objectId, createdAt}` and its
29/// update response is `{updatedAt}`, and returning more here would tempt a caller into sending
30/// more than parse-server does.
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct WriteResult {
33    pub object_id: String,
34}
35
36/// Storage operations.
37///
38/// `async fn` in trait, so this is not object-safe. That is deliberate for now: the server holds
39/// one concrete adapter chosen at construction, and boxing every call to support a `dyn` we do
40/// not need would cost allocations on the hot path. If a deployment ever needs to swap adapters
41/// at runtime, add a boxed wrapper rather than degrading this.
42pub trait StorageAdapter: Send + Sync {
43    /// Load every class schema. Upstream has no per-class fetch: a miss on any class triggers a
44    /// full `getAllClasses`, and reproducing that shape keeps the caching behavior comparable.
45    fn all_schemas(&self) -> impl Future<Output = Result<Vec<ClassSchema>, ParseError>> + Send;
46
47    /// Persist a class schema, creating the class if it does not exist.
48    fn upsert_schema(
49        &self,
50        schema: &ClassSchema,
51    ) -> impl Future<Output = Result<(), ParseError>> + Send;
52
53    /// Insert one row. `object_id` is generated by the caller, not the adapter, because it is
54    /// part of the Parse contract rather than a storage detail.
55    fn create(
56        &self,
57        schema: &ClassSchema,
58        row: &Row,
59    ) -> impl Future<Output = Result<WriteResult, ParseError>> + Send;
60
61    /// Find rows matching every constraint.
62    fn find(
63        &self,
64        schema: &ClassSchema,
65        constraints: &[Constraint],
66        options: &QueryOptions,
67    ) -> impl Future<Output = Result<Vec<Row>, ParseError>> + Send;
68
69    /// Count rows matching every constraint.
70    fn count(
71        &self,
72        schema: &ClassSchema,
73        constraints: &[Constraint],
74    ) -> impl Future<Output = Result<u64, ParseError>> + Send;
75
76    /// Update matching rows with the given field values.
77    ///
78    /// Returns how many rows matched, so a caller can distinguish "updated nothing because the
79    /// object does not exist" from "updated nothing because the ACL excluded it". Upstream
80    /// conflates those into `OBJECT_NOT_FOUND`, which is the behavior to reproduce at the REST
81    /// layer, but the adapter should not throw the information away before then.
82    fn update(
83        &self,
84        schema: &ClassSchema,
85        constraints: &[Constraint],
86        values: &Row,
87    ) -> impl Future<Output = Result<u64, ParseError>> + Send;
88
89    /// Delete matching rows. Returns how many, for the same reason as `update`.
90    fn delete(
91        &self,
92        schema: &ClassSchema,
93        constraints: &[Constraint],
94    ) -> impl Future<Output = Result<u64, ParseError>> + Send;
95
96    /// Create a unique index.
97    ///
98    /// **Index names are part of the contract.** Both adapters recover `duplicated_field` by regex
99    /// over the index name, and the Mongo regex matches only auto-generated `<field>_1` names, so
100    /// a differently-named index changes the error a client sees. `name: None` means "let the
101    /// backend auto-name it", which is what produces `username_1`.
102    fn ensure_unique_index(
103        &self,
104        class_name: &str,
105        fields: &[&str],
106        name: Option<&str>,
107    ) -> impl Future<Output = Result<(), ParseError>> + Send;
108}