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}