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}