Skip to main content

thingd_core/
store.rs

1//! Storage traits implemented by thingd storage adapters.
2
3use crate::model::{ListEventsOptions, ListObjectsOptions, PutObjectOptions};
4use crate::{
5    MemoryEvent, MemoryObject, QueueClaimOptions, QueueJob, QueueNackOptions, ThingdResult,
6};
7
8/// Object storage operations.
9///
10/// # Examples
11///
12/// ```rust
13/// use thingd_core::{MemoryEngine, ObjectStore, MemoryObject};
14///
15/// let mut store = MemoryEngine::new();
16/// let obj = MemoryObject::new("users", "alice", r#"{"name":"Alice"}"#);
17/// store.put_object(obj).unwrap();
18///
19/// let user = store.get_object("users", "alice").unwrap();
20/// assert!(user.is_some());
21/// assert_eq!(store.count_objects().unwrap(), 1);
22/// ```
23pub trait ObjectStore {
24    /// Insert or replace an object.
25    ///
26    /// # Errors
27    ///
28    /// Returns an error when the backing store cannot persist the object.
29    fn put_object(&mut self, object: MemoryObject) -> ThingdResult<MemoryObject>;
30
31    /// Insert or replace multiple objects in a single transaction.
32    ///
33    /// This is significantly faster than calling `put_object` in a loop
34    /// because it avoids per-object transaction overhead.
35    ///
36    /// # Errors
37    ///
38    /// Returns an error when the backing store cannot persist any object.
39    fn put_objects_batch(&mut self, objects: Vec<MemoryObject>) -> ThingdResult<Vec<MemoryObject>> {
40        let mut results = Vec::with_capacity(objects.len());
41        for object in objects {
42            results.push(self.put_object(object)?);
43        }
44        Ok(results)
45    }
46
47    /// Insert or replace an object with explicit options.
48    ///
49    /// When `options.index` is `false`, the FTS search index is not updated.
50    /// Use this when only metadata changes (e.g. timestamp dedup) and the body
51    /// text is identical — avoids wasted FTS DELETE + INSERT.
52    ///
53    /// # Errors
54    ///
55    /// Returns an error when the backing store cannot persist the object.
56    fn put_object_with_options(
57        &mut self,
58        object: MemoryObject,
59        options: PutObjectOptions,
60    ) -> ThingdResult<MemoryObject> {
61        let _ = options;
62        self.put_object(object)
63    }
64
65    /// Read an object by collection and id.
66    ///
67    /// # Errors
68    ///
69    /// Returns an error when the backing store cannot read the object.
70    fn get_object(&self, collection: &str, id: &str) -> ThingdResult<Option<MemoryObject>>;
71
72    /// List objects in one or more collections, with optional filtering, limit, and offset.
73    ///
74    /// Pass an empty `ListObjectsOptions` to return all objects across all collections.
75    ///
76    /// # Errors
77    ///
78    /// Returns an error when the backing store cannot list objects.
79    fn list_objects(
80        &self,
81        collections: Option<&[String]>,
82        options: &ListObjectsOptions,
83    ) -> ThingdResult<Vec<MemoryObject>>;
84
85    /// Delete an object by collection and id.
86    ///
87    /// # Errors
88    ///
89    /// Returns an error when the backing store cannot delete the object.
90    fn delete_object(&mut self, collection: &str, id: &str) -> ThingdResult<bool>;
91
92    /// Delete multiple objects in a single transaction.
93    ///
94    /// Returns the number of deleted objects. The `SQLite` adapter emits a bulk
95    /// `DELETE` statement in one transaction. The default implementation loops
96    /// calling `delete_object`.
97    ///
98    /// # Errors
99    ///
100    /// Returns an error when the backing store cannot delete any object.
101    fn delete_objects_batch(&mut self, keys: &[(String, String)]) -> ThingdResult<u64> {
102        let mut count = 0u64;
103        for (collection, id) in keys {
104            if self.delete_object(collection, id)? {
105                count += 1;
106            }
107        }
108        Ok(count)
109    }
110
111    /// Count total objects across all collections.
112    ///
113    /// # Errors
114    ///
115    /// Returns an error when the backing store cannot count objects.
116    fn count_objects(&self) -> ThingdResult<u64>;
117
118    /// List all unique collection names.
119    ///
120    /// # Errors
121    ///
122    /// Returns an error when the backing store cannot list collections.
123    fn list_collections(&self) -> ThingdResult<Vec<String>>;
124}
125
126/// Append-only event log operations.
127///
128/// # Examples
129///
130/// ```rust
131/// use thingd_core::{MemoryEngine, EventLog, MemoryEvent, ListEventsOptions};
132///
133/// let mut store = MemoryEngine::new();
134/// let event = MemoryEvent::new("audit", "user.created", r#"{"user":"alice"}"#);
135/// store.append_event(event).unwrap();
136///
137/// let events = store.list_events(None, ListEventsOptions::default()).unwrap();
138/// assert_eq!(events.len(), 1);
139/// assert_eq!(events[0].event_type, "user.created");
140/// ```
141pub trait EventLog {
142    /// Append an event to a stream.
143    ///
144    /// # Errors
145    ///
146    /// Returns an error when the backing store cannot append the event.
147    fn append_event(&mut self, event: MemoryEvent) -> ThingdResult<MemoryEvent>;
148
149    /// Append multiple events to a stream in a single transaction.
150    ///
151    /// This is significantly faster than calling `append_event` in a loop.
152    ///
153    /// # Errors
154    ///
155    /// Returns an error when the backing store cannot append any event.
156    fn append_events_batch(&mut self, events: Vec<MemoryEvent>) -> ThingdResult<Vec<MemoryEvent>> {
157        let mut results = Vec::with_capacity(events.len());
158        for event in events {
159            results.push(self.append_event(event)?);
160        }
161        Ok(results)
162    }
163
164    /// List events, optionally filtered by stream, with pagination.
165    ///
166    /// # Errors
167    ///
168    /// Returns an error when the backing store cannot read events.
169    fn list_events(
170        &self,
171        stream: Option<&str>,
172        options: ListEventsOptions,
173    ) -> ThingdResult<Vec<MemoryEvent>>;
174
175    /// Count total events across all streams.
176    ///
177    /// # Errors
178    ///
179    /// Returns an error when the backing store cannot count events.
180    fn count_events(&self) -> ThingdResult<u64>;
181
182    /// List all unique stream names.
183    ///
184    /// # Errors
185    ///
186    /// Returns an error when the backing store cannot list streams.
187    fn list_streams(&self) -> ThingdResult<Vec<String>>;
188}
189
190/// Queue storage operations.
191///
192/// # Examples
193///
194/// ```rust
195/// use thingd_core::{MemoryEngine, QueueStore, QueueJob, QueueJobStatus};
196///
197/// let mut store = MemoryEngine::new();
198/// let job = QueueJob::new("emails", "job-1", r#"{"to":"alice@example.com"}"#, 3);
199/// store.push_job(job).unwrap();
200///
201/// let claimed = store.claim_job("emails").unwrap();
202/// assert!(claimed.is_some());
203/// let job = claimed.unwrap();
204/// assert_eq!(job.status, QueueJobStatus::Leased);
205///
206/// let completed = store.ack_job("emails", &job.id).unwrap();
207/// assert_eq!(completed.unwrap().status, QueueJobStatus::Completed);
208/// ```
209pub trait QueueStore {
210    /// Push a job onto a queue.
211    ///
212    /// # Errors
213    ///
214    /// Returns an error when the backing store cannot persist the job.
215    fn push_job(&mut self, job: QueueJob) -> ThingdResult<QueueJob>;
216
217    /// Push multiple jobs onto a queue in a single transaction.
218    ///
219    /// This is significantly faster than calling `push_job` in a loop.
220    ///
221    /// # Errors
222    ///
223    /// Returns an error when the backing store cannot persist any job.
224    fn push_jobs_batch(&mut self, jobs: Vec<QueueJob>) -> ThingdResult<Vec<QueueJob>> {
225        let mut results = Vec::with_capacity(jobs.len());
226        for job in jobs {
227            results.push(self.push_job(job)?);
228        }
229        Ok(results)
230    }
231
232    /// Claim the next ready job from a queue.
233    ///
234    /// # Errors
235    ///
236    /// Returns an error when the backing store cannot claim a job.
237    fn claim_job(&mut self, queue: &str) -> ThingdResult<Option<QueueJob>> {
238        self.claim_job_with_options(queue, QueueClaimOptions::default())
239    }
240
241    /// Claim the next ready job from a queue with explicit options.
242    ///
243    /// # Errors
244    ///
245    /// Returns an error when the backing store cannot claim a job.
246    fn claim_job_with_options(
247        &mut self,
248        queue: &str,
249        options: QueueClaimOptions,
250    ) -> ThingdResult<Option<QueueJob>>;
251
252    /// Acknowledge a leased job as completed.
253    ///
254    /// # Errors
255    ///
256    /// Returns an error when the backing store cannot update the job.
257    fn ack_job(&mut self, queue: &str, id: &str) -> ThingdResult<Option<QueueJob>>;
258
259    /// Claim and immediately ack a job in a single transaction.
260    ///
261    /// This is faster than calling `claim_job` + `ack_job` separately
262    /// because it avoids per-operation transaction overhead.
263    ///
264    /// # Errors
265    ///
266    /// Returns an error when the backing store cannot claim or ack the job.
267    fn claim_and_ack(
268        &mut self,
269        queue: &str,
270        options: QueueClaimOptions,
271    ) -> ThingdResult<Option<QueueJob>> {
272        if let Some(job) = self.claim_job_with_options(queue, options)? {
273            self.ack_job(queue, &job.id)
274        } else {
275            Ok(None)
276        }
277    }
278
279    /// Reject a leased job for retry or dead-letter routing.
280    ///
281    /// # Errors
282    ///
283    /// Returns an error when the backing store cannot update the job.
284    fn nack_job(&mut self, queue: &str, id: &str) -> ThingdResult<Option<QueueJob>> {
285        self.nack_job_with_options(queue, id, QueueNackOptions::default())
286    }
287
288    /// Reject a leased job for retry or dead-letter routing with explicit options.
289    ///
290    /// # Errors
291    ///
292    /// Returns an error when the backing store cannot update the job.
293    fn nack_job_with_options(
294        &mut self,
295        queue: &str,
296        id: &str,
297        options: QueueNackOptions,
298    ) -> ThingdResult<Option<QueueJob>>;
299
300    /// List all jobs in a queue.
301    ///
302    /// # Errors
303    ///
304    /// Returns an error when the backing store cannot read queue jobs.
305    fn list_jobs(&self, queue: &str) -> ThingdResult<Vec<QueueJob>>;
306
307    /// List dead-letter jobs in a queue.
308    ///
309    /// # Errors
310    ///
311    /// Returns an error when the backing store cannot read dead-letter jobs.
312    fn list_dead_jobs(&self, queue: &str) -> ThingdResult<Vec<QueueJob>>;
313
314    /// List all unique queue names.
315    ///
316    /// # Errors
317    ///
318    /// Returns an error when the backing store cannot list queues.
319    fn list_queues(&self) -> ThingdResult<Vec<String>>;
320
321    /// Count total active jobs across all queues.
322    ///
323    /// # Errors
324    ///
325    /// Returns an error when the backing store cannot count active jobs.
326    fn count_active_jobs(&self) -> ThingdResult<u64>;
327
328    /// Count total dead-letter jobs across all queues.
329    ///
330    /// # Errors
331    ///
332    /// Returns an error when the backing store cannot count dead jobs.
333    fn count_dead_jobs(&self) -> ThingdResult<u64>;
334}
335
336/// Search operations.
337///
338/// # Examples
339///
340/// ```rust
341/// use thingd_core::{MemoryEngine, ObjectStore, Searcher, MemoryObject, SearchOptions};
342///
343/// let mut store = MemoryEngine::new();
344/// store.put_object(MemoryObject::new("docs", "readme", "Getting started guide")).unwrap();
345///
346/// let results = store.search("getting started", SearchOptions::default()).unwrap();
347/// assert!(!results.is_empty());
348/// ```
349pub trait Searcher {
350    /// Search memory objects and event logs by query text.
351    ///
352    /// # Errors
353    ///
354    /// Returns an error when search query fails.
355    fn search(
356        &self,
357        query: &str,
358        options: crate::SearchOptions,
359    ) -> ThingdResult<Vec<crate::SearchHit>>;
360}
361
362/// Graph link operations.
363pub trait LinkStore {
364    /// Create a new graph link.
365    ///
366    /// # Errors
367    ///
368    /// Returns an error when the link cannot be persisted.
369    fn create_link(&mut self, link: crate::Link) -> ThingdResult<crate::Link>;
370
371    /// Delete a graph link by id.
372    ///
373    /// # Errors
374    ///
375    /// Returns an error when the link cannot be deleted.
376    fn delete_link(&mut self, id: &str) -> ThingdResult<bool>;
377
378    /// Get a graph link by id.
379    ///
380    /// # Errors
381    ///
382    /// Returns an error when the link cannot be read.
383    fn get_link(&self, id: &str) -> ThingdResult<Option<crate::Link>>;
384
385    /// Get neighbors of a reference (outgoing, incoming, or both).
386    ///
387    /// # Errors
388    ///
389    /// Returns an error when neighbors cannot be queried.
390    fn get_neighbors(
391        &self,
392        reference: &str,
393        direction: crate::LinkDirection,
394        options: crate::LinkQueryOptions,
395    ) -> ThingdResult<Vec<crate::Link>>;
396
397    /// Count total links.
398    ///
399    /// # Errors
400    ///
401    /// Returns an error when count fails.
402    fn count_links(&self) -> ThingdResult<u64>;
403}
404
405/// Full storage interface expected from thingd engine adapters.
406pub trait ThingStore: EventLog + ObjectStore + QueueStore + Searcher + LinkStore {}
407
408impl<T> ThingStore for T where T: EventLog + ObjectStore + QueueStore + Searcher + LinkStore {}