hidane_core/store/mod.rs
1//! The storage boundary (ADR 0002).
2//!
3//! Everything above this module (gRPC, REST, Listen, transactions) talks to [`Store`]; the
4//! engine behind it can change without touching them. [`MemoryStore`] is the engine for now.
5//!
6//! The model:
7//!
8//! - Documents are **versioned by commit time** (microseconds, unique and increasing per
9//! database). A read names a [`ReadTime`] and sees the newest version at or before it, which
10//! gives consistent snapshots for queries, transactions and Listen.
11//! - A commit runs a closure against a [`WriteBatch`]: the commit time is fixed before the
12//! closure runs (server timestamps need it) and reads inside the batch see the batch's own
13//! writes. The closure's error aborts the commit with nothing applied.
14//! - A commit returns every changed document with its old and new version, so change
15//! notification costs O(changed documents), never O(stored documents).
16
17mod memory;
18
19use std::{fmt, ops::ControlFlow, sync::Arc};
20
21use hidane_proto::google::firestore::v1::{ArrayValue, MapValue, Value, value::ValueType};
22use hidane_proto::ordered::Fields;
23use prost::Message as _;
24use prost_types::Timestamp;
25
26pub use memory::MemoryStore;
27
28use crate::path::ResourcePath;
29
30/// A point in a database's history, in microseconds since the Unix epoch. Commit times are
31/// read times; reading at a commit time sees that commit.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
33pub struct ReadTime(pub i64);
34
35impl ReadTime {
36 pub const MAX: Self = Self(i64::MAX);
37
38 /// Truncates to microseconds, as Firestore does.
39 pub fn from_timestamp(ts: &Timestamp) -> Self {
40 Self(ts.seconds * 1_000_000 + i64::from(ts.nanos) / 1_000)
41 }
42
43 pub fn to_timestamp(self) -> Timestamp {
44 #[allow(clippy::cast_possible_truncation)]
45 Timestamp {
46 seconds: self.0.div_euclid(1_000_000),
47 nanos: (self.0.rem_euclid(1_000_000) * 1_000) as i32,
48 }
49 }
50}
51
52/// A stored document version.
53///
54/// Fields are kept protobuf-encoded: a document then costs about its wire size instead of the
55/// decoded tree (a two-field document is ~30 bytes encoded, over 600 decoded, because every
56/// map node reserves room for eleven entries). Readers decode with [`StoredDocument::fields`].
57#[derive(Debug, Clone, PartialEq)]
58pub struct StoredDocument {
59 /// Shared by every version of the same document.
60 pub path: Arc<ResourcePath>,
61 encoded_fields: Box<[u8]>,
62 pub create_time: Timestamp,
63 pub update_time: Timestamp,
64}
65
66impl StoredDocument {
67 pub fn new(
68 path: Arc<ResourcePath>,
69 fields: Fields,
70 create_time: Timestamp,
71 update_time: Timestamp,
72 ) -> Self {
73 Self {
74 path,
75 encoded_fields: MapValue { fields }.encode_to_vec().into_boxed_slice(),
76 create_time,
77 update_time,
78 }
79 }
80
81 /// The document's fields, decoded.
82 pub fn fields(&self) -> Fields {
83 MapValue::decode(&*self.encoded_fields)
84 .expect("fields were encoded by StoredDocument::new")
85 .fields
86 }
87
88 /// Size of the stored field encoding, in bytes.
89 pub fn encoded_len(&self) -> usize {
90 self.encoded_fields.len()
91 }
92
93 /// Whether the document holds exactly `fields`, bit for bit: unlike `==` on the decoded
94 /// fields, a NaN equals itself and `-0.0` differs from `0.0`, as for the official emulator,
95 /// to which writing either over itself changes nothing (`tools/oracle/negative_zero.py`).
96 /// The order of the fields does not matter.
97 pub fn has_fields(&self, fields: &Fields) -> bool {
98 let canonical = |fields: &Fields| {
99 MapValue {
100 fields: canonical(fields),
101 }
102 .encode_to_vec()
103 };
104 fields.len() == self.fields().len() && canonical(fields) == canonical(&self.fields())
105 }
106}
107
108/// Whether `a` and `b` are the same value, bit for bit, as [`StoredDocument::has_fields`]
109/// compares documents: a NaN equals itself, `-0.0` differs from `0.0`, and the order of a map's
110/// fields does not matter.
111pub fn identical(a: &Value, b: &Value) -> bool {
112 canonical_value(a).encode_to_vec() == canonical_value(b).encode_to_vec()
113}
114
115/// `fields` with every map, nested ones included, in UTF-8 key order.
116fn canonical(fields: &Fields) -> Fields {
117 fields
118 .sorted()
119 .into_iter()
120 .map(|(key, value)| (key.clone(), canonical_value(value)))
121 .collect()
122}
123
124fn canonical_value(value: &Value) -> Value {
125 let value_type = match &value.value_type {
126 Some(ValueType::MapValue(map)) => ValueType::MapValue(MapValue {
127 fields: canonical(&map.fields),
128 }),
129 Some(ValueType::ArrayValue(array)) => ValueType::ArrayValue(ArrayValue {
130 values: array.values.iter().map(canonical_value).collect(),
131 }),
132 _ => return value.clone(),
133 };
134 Value {
135 value_type: Some(value_type),
136 }
137}
138
139/// One document touched by a commit.
140#[derive(Debug, Clone)]
141pub struct Change {
142 pub path: Arc<ResourcePath>,
143 pub before: Option<Arc<StoredDocument>>,
144 pub after: Option<Arc<StoredDocument>>,
145}
146
147/// The result of a successful commit.
148#[derive(Debug, Clone)]
149pub struct Commit {
150 pub commit_time: ReadTime,
151 pub changes: Vec<Change>,
152}
153
154/// Why a commit was refused. Maps one to one onto gRPC status codes.
155#[derive(Debug, Clone, PartialEq, Eq)]
156pub enum StoreError {
157 InvalidArgument(String),
158 NotFound(String),
159 AlreadyExists(String),
160 FailedPrecondition(String),
161 Aborted(String),
162}
163
164impl fmt::Display for StoreError {
165 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166 match self {
167 Self::InvalidArgument(m)
168 | Self::NotFound(m)
169 | Self::AlreadyExists(m)
170 | Self::FailedPrecondition(m)
171 | Self::Aborted(m) => f.write_str(m),
172 }
173 }
174}
175
176impl std::error::Error for StoreError {}
177
178/// The writes of one commit, with read-your-writes visibility.
179pub trait WriteBatch {
180 /// The time this commit will be stamped with (and the documents' `update_time`).
181 fn commit_time(&self) -> ReadTime;
182
183 /// The document as it will be if the commit succeeds now: the batch's own writes first,
184 /// then the latest committed version.
185 fn get(&self, path: &ResourcePath) -> Option<Arc<StoredDocument>>;
186
187 /// Replaces the document's fields. Keeps `create_time` if the document exists.
188 fn set(&mut self, path: &ResourcePath, fields: Fields);
189
190 /// Deletes the document. Deleting a missing document is not an error.
191 fn delete(&mut self, path: &ResourcePath);
192
193 /// When a commit deleted the document, if it is missing because one did (not when it never
194 /// existed, nor when a delete found it missing): the commit time of that deletion, which
195 /// is the version a version precondition compares with.
196 fn deleted_at(&self, path: &ResourcePath) -> Option<ReadTime>;
197
198 /// Drops the batch's writes to the document, which keeps its latest committed version: a
199 /// commit whose writes leave a document as it was changes nothing.
200 fn revert(&mut self, path: &ResourcePath);
201}
202
203/// Called for each document of a scan, in `__name__` order; return `Break` to stop early.
204pub type Visit<'a> = dyn FnMut(&Arc<StoredDocument>) -> ControlFlow<()> + 'a;
205
206/// An entry of a collection listing.
207#[derive(Debug)]
208pub enum ListItem<'a> {
209 Document(&'a Arc<StoredDocument>),
210 /// No document exists at this path, but documents exist below it (in subcollections).
211 Missing(&'a ResourcePath),
212}
213
214/// Options of [`Store::list_collection`].
215#[derive(Debug, Default, Clone, Copy)]
216pub struct ListOptions<'a> {
217 /// Resume after this document path (exclusive), for paging.
218 pub after: Option<&'a ResourcePath>,
219 /// Also report missing documents that have descendants (`ListDocuments.show_missing`).
220 pub include_missing: bool,
221}
222
223/// The storage engine. `database` is the full database name,
224/// `projects/{project}/databases/{database}`; databases are created on first write.
225pub trait Store: Send + Sync {
226 /// A read time that sees every commit made so far.
227 fn latest_read_time(&self, database: &str) -> ReadTime;
228
229 /// The oldest read time still served; older versions may have been dropped.
230 fn earliest_read_time(&self, database: &str) -> ReadTime;
231
232 fn get(&self, database: &str, path: &ResourcePath, at: ReadTime)
233 -> Option<Arc<StoredDocument>>;
234
235 /// The documents directly in `collection` (not in its subcollections).
236 fn scan_collection(
237 &self,
238 database: &str,
239 collection: &ResourcePath,
240 at: ReadTime,
241 visit: &mut Visit<'_>,
242 ) {
243 self.list_collection(
244 database,
245 collection,
246 at,
247 ListOptions::default(),
248 &mut |item| match item {
249 ListItem::Document(doc) => visit(doc),
250 ListItem::Missing(_) => ControlFlow::Continue(()),
251 },
252 );
253 }
254
255 /// Like [`Store::scan_collection`], with paging and missing documents.
256 fn list_collection(
257 &self,
258 database: &str,
259 collection: &ResourcePath,
260 at: ReadTime,
261 options: ListOptions<'_>,
262 visit: &mut dyn FnMut(ListItem<'_>) -> ControlFlow<()>,
263 );
264
265 /// Every document under `parent` (a document path, or the root) whose collection ID is
266 /// `collection_id`, at any depth, ordered by full path.
267 fn scan_collection_group(
268 &self,
269 database: &str,
270 parent: &ResourcePath,
271 collection_id: &str,
272 at: ReadTime,
273 visit: &mut Visit<'_>,
274 );
275
276 /// Every document below `parent` (a document path, or the root), at any depth, ordered by
277 /// path. `parent` itself is not included.
278 fn scan_descendants(
279 &self,
280 database: &str,
281 parent: &ResourcePath,
282 at: ReadTime,
283 visit: &mut Visit<'_>,
284 );
285
286 /// IDs of the collections directly under `parent` that contain at least one document at
287 /// any depth, in UTF-8 order.
288 fn list_collection_ids(
289 &self,
290 database: &str,
291 parent: &ResourcePath,
292 at: ReadTime,
293 ) -> Vec<String>;
294
295 /// Runs `write` as one atomic commit.
296 fn commit(
297 &self,
298 database: &str,
299 write: &mut dyn FnMut(&mut dyn WriteBatch) -> Result<(), StoreError>,
300 ) -> Result<Commit, StoreError>;
301
302 /// Drops every document of `database` and every older version, at a time after its last
303 /// commit, which it returns: later commits are later still, so listeners can order the
304 /// clear among them.
305 fn clear_database(&self, database: &str) -> ReadTime;
306
307 /// The databases that hold or held documents.
308 fn database_names(&self) -> Vec<String>;
309
310 /// Drops every database.
311 fn clear(&self);
312}