Skip to main content

thingd/
store.rs

1//! Storage traits implemented by thingd storage adapters.
2
3use crate::model::{
4    AggregateOptions, AggregateResult, CollectionSchema, ListEventsOptions, ListObjectsOptions,
5    PutObjectOptions, SchemaOptions, TimeSeriesOptions, TimeSeriesResult,
6};
7use crate::{
8    MemoryEvent, MemoryObject, QueueClaimOptions, QueueJob, QueueNackOptions, ThingdError,
9    ThingdResult,
10};
11
12/// Object storage operations.
13///
14/// # Examples
15///
16/// ```rust
17/// use thingd::{MemoryEngine, ObjectStore, MemoryObject};
18///
19/// let mut store = MemoryEngine::new();
20/// let obj = MemoryObject::new("users", "alice", r#"{"name":"Alice"}"#);
21/// store.put_object(obj).unwrap();
22///
23/// let user = store.get_object("users", "alice").unwrap();
24/// assert!(user.is_some());
25/// assert_eq!(store.count_objects().unwrap(), 1);
26/// ```
27pub trait ObjectStore {
28    /// Insert or replace an object.
29    ///
30    /// # Errors
31    ///
32    /// Returns an error when the backing store cannot persist the object.
33    fn put_object(&mut self, object: MemoryObject) -> ThingdResult<MemoryObject>;
34
35    /// Insert or replace multiple objects in a single transaction.
36    ///
37    /// This is significantly faster than calling `put_object` in a loop
38    /// because it avoids per-object transaction overhead.
39    ///
40    /// **Atomicity:** The `SQLite` adapter wraps all writes in a single
41    /// transaction — a failure rolls back all changes. The in-memory
42    /// default implementation loops calling `put_object` without a
43    /// transaction, so a partial failure may leave some objects written.
44    ///
45    /// # Errors
46    ///
47    /// Returns an error when the backing store cannot persist any object.
48    fn put_objects_batch(&mut self, objects: Vec<MemoryObject>) -> ThingdResult<Vec<MemoryObject>> {
49        let mut results = Vec::with_capacity(objects.len());
50        for object in objects {
51            results.push(self.put_object(object)?);
52        }
53        Ok(results)
54    }
55
56    /// Insert or replace an object with explicit options.
57    ///
58    /// When `options.index` is `false`, the FTS search index is not updated.
59    /// Use this when only metadata changes (e.g. timestamp dedup) and the body
60    /// text is identical — avoids wasted FTS DELETE + INSERT.
61    ///
62    /// When `options.expected_version` is `Some(v)`, the operation succeeds only
63    /// if the current version of the object equals `v` (optimistic locking / CAS).
64    /// Returns `ThingdError::Conflict` on version mismatch.
65    ///
66    /// # Errors
67    ///
68    /// Returns an error when the backing store cannot persist the object.
69    fn put_object_with_options(
70        &mut self,
71        object: MemoryObject,
72        options: PutObjectOptions,
73    ) -> ThingdResult<MemoryObject> {
74        let _ = options;
75        self.put_object(object)
76    }
77
78    /// Read an object by collection and id.
79    ///
80    /// # Errors
81    ///
82    /// Returns an error when the backing store cannot read the object.
83    fn get_object(&self, collection: &str, id: &str) -> ThingdResult<Option<MemoryObject>>;
84
85    /// List objects in one or more collections, with optional filtering, limit, and offset.
86    ///
87    /// Pass an empty `ListObjectsOptions` to return all objects across all collections.
88    ///
89    /// # Errors
90    ///
91    /// Returns an error when the backing store cannot list objects.
92    fn list_objects(
93        &self,
94        collections: Option<&[String]>,
95        options: &ListObjectsOptions,
96    ) -> ThingdResult<Vec<MemoryObject>>;
97
98    /// Delete an object by collection and id.
99    ///
100    /// # Errors
101    ///
102    /// Returns an error when the backing store cannot delete the object.
103    fn delete_object(&mut self, collection: &str, id: &str) -> ThingdResult<bool>;
104
105    /// Delete multiple objects in a single transaction.
106    ///
107    /// Returns the number of deleted objects. The `SQLite` adapter emits a bulk
108    /// `DELETE` statement in one transaction. The default implementation loops
109    /// calling `delete_object`.
110    ///
111    /// **Atomicity:** The `SQLite` adapter wraps all deletes in a single
112    /// transaction — a failure rolls back all deletions. The in-memory
113    /// default implementation loops calling `delete_object` without a
114    /// transaction, so a partial failure may leave some objects deleted
115    /// and others not.
116    ///
117    /// # Errors
118    ///
119    /// Returns an error when the backing store cannot delete any object.
120    fn delete_objects_batch(&mut self, keys: &[(String, String)]) -> ThingdResult<u64> {
121        let mut count = 0u64;
122        for (collection, id) in keys {
123            if self.delete_object(collection, id)? {
124                count += 1;
125            }
126        }
127        Ok(count)
128    }
129
130    /// Count total objects across all collections.
131    ///
132    /// # Errors
133    ///
134    /// Returns an error when the backing store cannot count objects.
135    fn count_objects(&self) -> ThingdResult<u64>;
136
137    /// List all unique collection names.
138    ///
139    /// # Errors
140    ///
141    /// Returns an error when the backing store cannot list collections.
142    fn list_collections(&self) -> ThingdResult<Vec<String>>;
143
144    /// Reflect the schema of all or one collection by sampling stored objects.
145    ///
146    /// Returns inferred field names, types, and sample values. When `collection`
147    /// is `None`, returns schemas for all collections. When `Some(name)`, returns
148    /// the schema for that collection or an empty vec if not found.
149    ///
150    /// # Errors
151    ///
152    /// Returns an error when the backing store cannot read objects.
153    fn schema(
154        &self,
155        collection: Option<&str>,
156        options: &SchemaOptions,
157    ) -> ThingdResult<Vec<CollectionSchema>>;
158}
159
160/// Append-only event log operations.
161///
162/// # Examples
163///
164/// ```rust
165/// use thingd::{MemoryEngine, EventLog, MemoryEvent, ListEventsOptions};
166///
167/// let mut store = MemoryEngine::new();
168/// let event = MemoryEvent::new("audit", "user.created", r#"{"user":"alice"}"#);
169/// store.append_event(event).unwrap();
170///
171/// let events = store.list_events(None, ListEventsOptions::default()).unwrap();
172/// assert_eq!(events.len(), 1);
173/// assert_eq!(events[0].event_type, "user.created");
174/// ```
175pub trait EventLog {
176    /// Returns `true` if the stream is protected from deletion and external
177    /// mutation. Protected streams (e.g. `"__thingd:mcp:audit"`) reject
178    /// `delete_last_event` and `delete_stream` calls.
179    fn is_protected_stream(&self, stream: &str) -> bool {
180        let _ = stream;
181        false
182    }
183    /// Append an event to a stream.
184    ///
185    /// # Errors
186    ///
187    /// Returns an error when the backing store cannot append the event.
188    fn append_event(&mut self, event: MemoryEvent) -> ThingdResult<MemoryEvent>;
189
190    /// Append multiple events to a stream in a single transaction.
191    ///
192    /// This is significantly faster than calling `append_event` in a loop.
193    ///
194    /// # Errors
195    ///
196    /// Returns an error when the backing store cannot append any event.
197    fn append_events_batch(&mut self, events: Vec<MemoryEvent>) -> ThingdResult<Vec<MemoryEvent>> {
198        let mut results = Vec::with_capacity(events.len());
199        for event in events {
200            results.push(self.append_event(event)?);
201        }
202        Ok(results)
203    }
204
205    /// List events, optionally filtered by stream, with pagination.
206    ///
207    /// Events are returned in ascending sequence order (oldest first).
208    ///
209    /// # Errors
210    ///
211    /// Returns an error when the backing store cannot read events.
212    fn list_events(
213        &self,
214        stream: Option<&str>,
215        options: ListEventsOptions,
216    ) -> ThingdResult<Vec<MemoryEvent>>;
217
218    /// Delete the most recent event from a stream.
219    ///
220    /// Returns the deleted event, or `None` if the stream was empty or
221    /// did not exist. This is useful for implementing undo patterns in
222    /// event-sourced applications.
223    ///
224    /// Returns `ThingdError::Protected` when the stream is protected.
225    ///
226    /// # Errors
227    ///
228    /// Returns an error when the backing store cannot delete the event.
229    fn delete_last_event(&mut self, stream: &str) -> ThingdResult<Option<MemoryEvent>> {
230        if self.is_protected_stream(stream) {
231            return Err(ThingdError::Protected(format!(
232                "stream '{stream}' is protected and cannot be modified"
233            )));
234        }
235        Err(ThingdError::Storage(
236            "delete_last_event is not supported by this adapter".into(),
237        ))
238    }
239
240    /// Delete all events in a stream.
241    ///
242    /// Returns the number of events deleted. This is useful for cleaning
243    /// up completed or expired event streams (e.g. finished game matches).
244    ///
245    /// Returns `ThingdError::Protected` when the stream is protected.
246    ///
247    /// # Errors
248    ///
249    /// Returns an error when the backing store cannot delete the events.
250    fn delete_stream(&mut self, stream: &str) -> ThingdResult<u64> {
251        if self.is_protected_stream(stream) {
252            return Err(ThingdError::Protected(format!(
253                "stream '{stream}' is protected and cannot be modified"
254            )));
255        }
256        Err(ThingdError::Storage(
257            "delete_stream is not supported by this adapter".into(),
258        ))
259    }
260
261    /// Count total events across all streams.
262    ///
263    /// # Errors
264    ///
265    /// Returns an error when the backing store cannot count events.
266    fn count_events(&self) -> ThingdResult<u64>;
267
268    /// List all unique stream names.
269    ///
270    /// # Errors
271    ///
272    /// Returns an error when the backing store cannot list streams.
273    fn list_streams(&self) -> ThingdResult<Vec<String>>;
274}
275
276/// Queue storage operations.
277///
278/// # Examples
279///
280/// ```rust
281/// use thingd::{MemoryEngine, QueueStore, QueueJob, QueueJobStatus};
282///
283/// let mut store = MemoryEngine::new();
284/// let job = QueueJob::new("emails", "job-1", r#"{"to":"alice@example.com"}"#, 3);
285/// store.push_job(job).unwrap();
286///
287/// let claimed = store.claim_job("emails").unwrap();
288/// assert!(claimed.is_some());
289/// let job = claimed.unwrap();
290/// assert_eq!(job.status, QueueJobStatus::Leased);
291///
292/// let completed = store.ack_job("emails", &job.id).unwrap();
293/// assert_eq!(completed.unwrap().status, QueueJobStatus::Completed);
294/// ```
295pub trait QueueStore {
296    /// Push a job onto a queue.
297    ///
298    /// # Errors
299    ///
300    /// Returns an error when the backing store cannot persist the job.
301    fn push_job(&mut self, job: QueueJob) -> ThingdResult<QueueJob>;
302
303    /// Push multiple jobs onto a queue in a single transaction.
304    ///
305    /// This is significantly faster than calling `push_job` in a loop.
306    ///
307    /// # Errors
308    ///
309    /// Returns an error when the backing store cannot persist any job.
310    fn push_jobs_batch(&mut self, jobs: Vec<QueueJob>) -> ThingdResult<Vec<QueueJob>> {
311        let mut results = Vec::with_capacity(jobs.len());
312        for job in jobs {
313            results.push(self.push_job(job)?);
314        }
315        Ok(results)
316    }
317
318    /// Claim the next ready job from a queue.
319    ///
320    /// # Errors
321    ///
322    /// Returns an error when the backing store cannot claim a job.
323    fn claim_job(&mut self, queue: &str) -> ThingdResult<Option<QueueJob>> {
324        self.claim_job_with_options(queue, QueueClaimOptions::default())
325    }
326
327    /// Claim the next ready job from a queue with explicit options.
328    ///
329    /// # Errors
330    ///
331    /// Returns an error when the backing store cannot claim a job.
332    fn claim_job_with_options(
333        &mut self,
334        queue: &str,
335        options: QueueClaimOptions,
336    ) -> ThingdResult<Option<QueueJob>>;
337
338    /// Acknowledge a leased job as completed.
339    ///
340    /// # Errors
341    ///
342    /// Returns an error when the backing store cannot update the job.
343    fn ack_job(&mut self, queue: &str, id: &str) -> ThingdResult<Option<QueueJob>>;
344
345    /// Claim and immediately ack a job in a single transaction.
346    ///
347    /// This is faster than calling `claim_job` + `ack_job` separately
348    /// because it avoids per-operation transaction overhead.
349    ///
350    /// # Errors
351    ///
352    /// Returns an error when the backing store cannot claim or ack the job.
353    fn claim_and_ack(
354        &mut self,
355        queue: &str,
356        options: QueueClaimOptions,
357    ) -> ThingdResult<Option<QueueJob>> {
358        if let Some(job) = self.claim_job_with_options(queue, options)? {
359            self.ack_job(queue, &job.id)
360        } else {
361            Ok(None)
362        }
363    }
364
365    /// Reject a leased job for retry or dead-letter routing.
366    ///
367    /// # Errors
368    ///
369    /// Returns an error when the backing store cannot update the job.
370    fn nack_job(&mut self, queue: &str, id: &str) -> ThingdResult<Option<QueueJob>> {
371        self.nack_job_with_options(queue, id, QueueNackOptions::default())
372    }
373
374    /// Reject a leased job for retry or dead-letter routing with explicit options.
375    ///
376    /// # Errors
377    ///
378    /// Returns an error when the backing store cannot update the job.
379    fn nack_job_with_options(
380        &mut self,
381        queue: &str,
382        id: &str,
383        options: QueueNackOptions,
384    ) -> ThingdResult<Option<QueueJob>>;
385
386    /// List all jobs in a queue.
387    ///
388    /// # Errors
389    ///
390    /// Returns an error when the backing store cannot read queue jobs.
391    fn list_jobs(&self, queue: &str) -> ThingdResult<Vec<QueueJob>>;
392
393    /// List dead-letter jobs in a queue.
394    ///
395    /// # Errors
396    ///
397    /// Returns an error when the backing store cannot read dead-letter jobs.
398    fn list_dead_jobs(&self, queue: &str) -> ThingdResult<Vec<QueueJob>>;
399
400    /// List all unique queue names.
401    ///
402    /// # Errors
403    ///
404    /// Returns an error when the backing store cannot list queues.
405    fn list_queues(&self) -> ThingdResult<Vec<String>>;
406
407    /// Count total active jobs across all queues.
408    ///
409    /// # Errors
410    ///
411    /// Returns an error when the backing store cannot count active jobs.
412    fn count_active_jobs(&self) -> ThingdResult<u64>;
413
414    /// Count total dead-letter jobs across all queues.
415    ///
416    /// # Errors
417    ///
418    /// Returns an error when the backing store cannot count dead jobs.
419    fn count_dead_jobs(&self) -> ThingdResult<u64>;
420}
421
422/// Search operations.
423///
424/// # Examples
425///
426/// ```rust
427/// use thingd::{MemoryEngine, ObjectStore, Searcher, MemoryObject, SearchOptions};
428///
429/// let mut store = MemoryEngine::new();
430/// store.put_object(MemoryObject::new("docs", "readme", "Getting started guide")).unwrap();
431///
432/// let results = store.search("getting started", SearchOptions::default()).unwrap();
433/// assert!(!results.is_empty());
434/// ```
435pub trait Searcher {
436    /// Search memory objects and event logs by query text.
437    ///
438    /// # Errors
439    ///
440    /// Returns an error when search query fails.
441    fn search(
442        &self,
443        query: &str,
444        options: crate::SearchOptions,
445    ) -> ThingdResult<Vec<crate::SearchHit>>;
446}
447
448/// Graph link operations.
449pub trait LinkStore {
450    /// Create a new graph link.
451    ///
452    /// # Errors
453    ///
454    /// Returns an error when the link cannot be persisted.
455    fn create_link(&mut self, link: crate::Link) -> ThingdResult<crate::Link>;
456
457    /// Delete a graph link by id.
458    ///
459    /// # Errors
460    ///
461    /// Returns an error when the link cannot be deleted.
462    fn delete_link(&mut self, id: &str) -> ThingdResult<bool>;
463
464    /// Get a graph link by id.
465    ///
466    /// # Errors
467    ///
468    /// Returns an error when the link cannot be read.
469    fn get_link(&self, id: &str) -> ThingdResult<Option<crate::Link>>;
470
471    /// Get neighbors of a reference (outgoing, incoming, or both).
472    ///
473    /// # Errors
474    ///
475    /// Returns an error when neighbors cannot be queried.
476    fn get_neighbors(
477        &self,
478        reference: &str,
479        direction: crate::LinkDirection,
480        options: crate::LinkQueryOptions,
481    ) -> ThingdResult<Vec<crate::Link>>;
482
483    /// Count total links.
484    ///
485    /// # Errors
486    ///
487    /// Returns an error when count fails.
488    fn count_links(&self) -> ThingdResult<u64>;
489}
490
491/// Aggregation operations.
492pub trait AggregateStore {
493    /// Run a general aggregation query over objects in a collection.
494    ///
495    /// Supports count, sum, avg, min, max with optional `group_by`.
496    ///
497    /// # Errors
498    ///
499    /// Returns an error when the aggregation query fails.
500    fn aggregate(
501        &self,
502        collection: &str,
503        options: &AggregateOptions,
504    ) -> ThingdResult<AggregateResult>;
505
506    /// Run a time-bucketed aggregation query.
507    ///
508    /// Groups objects by hour/day/week/month and applies an aggregation function.
509    ///
510    /// # Errors
511    ///
512    /// Returns an error when the time-series query fails.
513    fn timeseries(
514        &self,
515        collection: &str,
516        options: &TimeSeriesOptions,
517    ) -> ThingdResult<TimeSeriesResult>;
518}
519
520/// Full storage interface expected from thingd engine adapters.
521pub trait ThingStore:
522    EventLog + ObjectStore + QueueStore + Searcher + LinkStore + AggregateStore
523{
524}
525
526impl<T> ThingStore for T where
527    T: EventLog + ObjectStore + QueueStore + Searcher + LinkStore + AggregateStore
528{
529}