Skip to main content

khive_storage/
types.rs

1//! Shared types used across storage capability traits.
2
3use std::fmt;
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use uuid::Uuid;
9
10use khive_types::{EdgeRelation, SubstrateKind};
11
12use crate::error::StorageError;
13
14pub type StorageResult<T> = Result<T, StorageError>;
15
16#[derive(Clone, Debug, Default, Serialize, Deserialize)]
17pub struct BatchWriteSummary {
18    pub attempted: u64,
19    pub affected: u64,
20    pub failed: u64,
21    #[serde(default, skip_serializing_if = "String::is_empty")]
22    pub first_error: String,
23}
24
25#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
26#[serde(rename_all = "snake_case")]
27pub enum DeleteMode {
28    Soft,
29    Hard,
30}
31
32// -- SQL primitives --
33
34#[derive(Clone, Debug, Serialize, Deserialize)]
35#[serde(rename_all = "snake_case")]
36pub enum SqlValue {
37    Null,
38    Bool(bool),
39    Integer(i64),
40    Float(f64),
41    Text(String),
42    Blob(Vec<u8>),
43    Json(Value),
44    Uuid(Uuid),
45    Timestamp(DateTime<Utc>),
46}
47
48#[derive(Clone, Debug, Serialize, Deserialize)]
49pub struct SqlStatement {
50    pub sql: String,
51    pub params: Vec<SqlValue>,
52    pub label: Option<String>,
53}
54
55#[derive(Clone, Debug, Serialize, Deserialize)]
56pub struct SqlColumn {
57    pub name: String,
58    pub value: SqlValue,
59}
60
61#[derive(Clone, Debug, Serialize, Deserialize)]
62pub struct SqlRow {
63    pub columns: Vec<SqlColumn>,
64}
65
66impl SqlRow {
67    pub fn get(&self, name: &str) -> Option<&SqlValue> {
68        self.columns
69            .iter()
70            .find(|c| c.name == name)
71            .map(|c| &c.value)
72    }
73}
74
75#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
76#[serde(rename_all = "snake_case")]
77pub enum SqlIsolation {
78    Default,
79    ReadCommitted,
80    RepeatableRead,
81    Serializable,
82}
83
84#[derive(Clone, Debug, Serialize, Deserialize)]
85pub struct SqlTxOptions {
86    pub read_only: bool,
87    pub isolation: SqlIsolation,
88    pub label: Option<String>,
89}
90
91impl Default for SqlTxOptions {
92    fn default() -> Self {
93        Self {
94            read_only: false,
95            isolation: SqlIsolation::Default,
96            label: None,
97        }
98    }
99}
100
101// -- Vector types --
102
103#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
104#[serde(rename_all = "snake_case")]
105pub enum VectorIndexKind {
106    Hnsw,
107    SqliteVec,
108    Flat,
109}
110
111/// Backend capability declaration for vector stores (ADR-041).
112///
113/// Returned by [`VectorStore::capabilities`]. Higher-level retrieval policy
114/// (hybrid search, HyDE fan-out, etc.) introspects this struct at construction
115/// time to select the optimal code path without relying on error-type matching.
116#[derive(Clone, Debug, Serialize, Deserialize)]
117pub struct VectorStoreCapabilities {
118    /// Supports metadata pre-filter pushdown into the index scan.
119    pub supports_filter: bool,
120    /// Supports batch search (multiple query vectors in one call).
121    pub supports_batch_search: bool,
122    /// Supports quantization (reduces memory; may trade recall).
123    pub supports_quantization: bool,
124    /// Supports in-place update without a delete+insert round-trip.
125    pub supports_update: bool,
126    /// Maximum supported embedding dimension, or `None` if unbounded.
127    pub max_dimensions: Option<u32>,
128    /// Index algorithms available in this backend.
129    pub index_kinds: Vec<VectorIndexKind>,
130}
131
132/// A typed predicate for backend-pushable metadata filtering (ADR-041).
133///
134/// Intentionally minimal: namespace isolation and kind scoping cover the v0.2
135/// hybrid-search cases. Range predicates and compound logic are deferred to a
136/// future retrieval ADR. Adding fields is non-breaking (serde defaults); removing
137/// fields is not.
138#[derive(Clone, Debug, Default, Serialize, Deserialize)]
139pub struct VectorMetadataFilter {
140    /// Restrict to these namespaces.
141    pub namespaces: Vec<String>,
142    /// Restrict to these substrate kinds.
143    pub kinds: Vec<SubstrateKind>,
144    /// Arbitrary key=value metadata predicates (equality only).
145    pub properties: Vec<(String, serde_json::Value)>,
146}
147
148impl VectorMetadataFilter {
149    /// Returns `true` when no predicates are set (filter is a no-op).
150    pub fn is_empty(&self) -> bool {
151        self.namespaces.is_empty() && self.kinds.is_empty() && self.properties.is_empty()
152    }
153}
154
155#[derive(Clone, Debug, Serialize, Deserialize)]
156pub struct VectorRecord {
157    pub subject_id: Uuid,
158    pub kind: SubstrateKind,
159    pub namespace: String,
160    pub embedding: Vec<f32>,
161    pub updated_at: DateTime<Utc>,
162}
163
164#[derive(Clone, Debug, Serialize, Deserialize)]
165pub struct VectorSearchRequest {
166    pub query_embedding: Vec<f32>,
167    pub top_k: u32,
168    pub namespace: Option<String>,
169    pub kind: Option<SubstrateKind>,
170}
171
172#[derive(Clone, Debug, Serialize, Deserialize)]
173pub struct VectorSearchHit {
174    pub subject_id: Uuid,
175    pub score: khive_score::DeterministicScore,
176    pub rank: u32,
177}
178
179#[derive(Clone, Debug, Serialize, Deserialize)]
180pub struct VectorStoreInfo {
181    pub model_name: String,
182    pub dimensions: usize,
183    pub index_kind: VectorIndexKind,
184    pub entry_count: u64,
185    pub needs_rebuild: bool,
186    pub last_rebuild_at: Option<DateTime<Utc>>,
187}
188
189// -- Text search types --
190
191#[derive(Clone, Debug, Serialize, Deserialize)]
192pub struct TextDocument {
193    pub subject_id: Uuid,
194    pub kind: SubstrateKind,
195    pub namespace: String,
196    pub title: Option<String>,
197    pub body: String,
198    pub tags: Vec<String>,
199    pub metadata: Option<Value>,
200    pub updated_at: DateTime<Utc>,
201}
202
203#[derive(Clone, Debug, Default, Serialize, Deserialize)]
204pub struct TextFilter {
205    pub ids: Vec<Uuid>,
206    pub kinds: Vec<SubstrateKind>,
207    pub namespaces: Vec<String>,
208}
209
210#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
211#[serde(rename_all = "snake_case")]
212pub enum TextQueryMode {
213    Plain,
214    Phrase,
215}
216
217#[derive(Clone, Debug, Serialize, Deserialize)]
218pub struct TextSearchRequest {
219    pub query: String,
220    pub mode: TextQueryMode,
221    pub filter: Option<TextFilter>,
222    pub top_k: u32,
223    pub snippet_chars: usize,
224}
225
226#[derive(Clone, Debug, Serialize, Deserialize)]
227pub struct TextSearchHit {
228    pub subject_id: Uuid,
229    pub score: khive_score::DeterministicScore,
230    pub rank: u32,
231    pub title: Option<String>,
232    pub snippet: Option<String>,
233}
234
235#[derive(Clone, Debug, Serialize, Deserialize)]
236pub struct TextIndexStats {
237    pub document_count: u64,
238    pub needs_rebuild: bool,
239    pub last_rebuild_at: Option<DateTime<Utc>>,
240}
241
242#[derive(Clone, Debug, Serialize, Deserialize)]
243#[serde(rename_all = "snake_case")]
244pub enum IndexRebuildScope {
245    Full,
246    Entities(Vec<Uuid>),
247}
248
249// -- Pagination --
250
251#[derive(Clone, Debug, Serialize, Deserialize)]
252pub struct PageRequest {
253    pub offset: u64,
254    pub limit: u32,
255}
256
257impl Default for PageRequest {
258    fn default() -> Self {
259        Self {
260            offset: 0,
261            limit: 50,
262        }
263    }
264}
265
266#[derive(Clone, Debug, Serialize, Deserialize)]
267pub struct Page<T> {
268    pub items: Vec<T>,
269    pub total: Option<u64>,
270}
271
272// -- Graph types --
273
274/// A type-safe link ID (wraps Uuid).
275#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
276pub struct LinkId(pub Uuid);
277
278impl From<Uuid> for LinkId {
279    fn from(u: Uuid) -> Self {
280        Self(u)
281    }
282}
283
284impl From<LinkId> for Uuid {
285    fn from(l: LinkId) -> Uuid {
286        l.0
287    }
288}
289
290impl fmt::Display for LinkId {
291    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
292        self.0.fmt(f)
293    }
294}
295
296/// A directed edge in the graph.
297#[derive(Clone, Debug, Serialize, Deserialize)]
298pub struct Edge {
299    pub id: LinkId,
300    pub source_id: Uuid,
301    pub target_id: Uuid,
302    pub relation: EdgeRelation,
303    pub weight: f64,
304    pub created_at: DateTime<Utc>,
305    pub metadata: Option<Value>,
306}
307
308#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
309#[serde(rename_all = "snake_case")]
310pub enum Direction {
311    #[default]
312    Out,
313    In,
314    Both,
315}
316
317#[derive(Clone, Debug, Default, Serialize, Deserialize)]
318pub struct TimeRange {
319    pub start: Option<DateTime<Utc>>,
320    pub end: Option<DateTime<Utc>>,
321}
322
323#[derive(Clone, Debug, Default, Serialize, Deserialize)]
324pub struct EdgeFilter {
325    pub ids: Vec<LinkId>,
326    pub source_ids: Vec<Uuid>,
327    pub target_ids: Vec<Uuid>,
328    pub relations: Vec<EdgeRelation>,
329    pub min_weight: Option<f64>,
330    pub max_weight: Option<f64>,
331    pub created_at: Option<TimeRange>,
332}
333
334#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
335#[serde(rename_all = "snake_case")]
336pub enum EdgeSortField {
337    CreatedAt,
338    Weight,
339    Relation,
340}
341
342#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
343#[serde(rename_all = "snake_case")]
344pub enum SortDirection {
345    Asc,
346    Desc,
347}
348
349#[derive(Clone, Debug, Serialize, Deserialize)]
350pub struct SortOrder<F> {
351    pub field: F,
352    pub direction: SortDirection,
353}
354
355#[derive(Clone, Debug, Serialize, Deserialize)]
356pub struct NeighborQuery {
357    pub direction: Direction,
358    pub relations: Option<Vec<EdgeRelation>>,
359    pub limit: Option<u32>,
360    pub min_weight: Option<f64>,
361}
362
363/// One neighbor returned by a graph query.
364///
365/// Field naming (#148): on the JSON wire, the node identifier is serialized as
366/// `id` (not `node_id`) so it matches the verb-wide identifier convention.
367/// Internal Rust code still uses `.node_id` on the struct.
368///
369/// Enrichment (#162): `name` and `kind` are populated by the runtime layer
370/// after the storage call returns. Storage `GraphStore` impls leave them
371/// `None`; the runtime batch-fetches the entity rows and fills them in.
372#[derive(Clone, Debug, Serialize, Deserialize)]
373pub struct NeighborHit {
374    #[serde(rename = "id")]
375    pub node_id: Uuid,
376    pub edge_id: Uuid,
377    pub relation: EdgeRelation,
378    pub weight: f64,
379    #[serde(default, skip_serializing_if = "Option::is_none")]
380    pub name: Option<String>,
381    #[serde(default, skip_serializing_if = "Option::is_none")]
382    pub kind: Option<String>,
383}
384
385#[derive(Clone, Debug, Default, Serialize, Deserialize)]
386pub struct TraversalOptions {
387    pub max_depth: usize,
388    pub direction: Direction,
389    pub relations: Option<Vec<EdgeRelation>>,
390    pub min_weight: Option<f64>,
391    pub limit: Option<u32>,
392}
393
394impl TraversalOptions {
395    pub fn new(max_depth: usize) -> Self {
396        Self {
397            max_depth,
398            ..Default::default()
399        }
400    }
401
402    pub fn with_direction(mut self, d: Direction) -> Self {
403        self.direction = d;
404        self
405    }
406}
407
408#[derive(Clone, Debug, Serialize, Deserialize)]
409pub struct TraversalRequest {
410    pub roots: Vec<Uuid>,
411    pub options: TraversalOptions,
412    pub include_roots: bool,
413}
414
415/// One node along a traversal path.
416///
417/// Field naming (#148): JSON wire serialization is `id`. Enrichment (#162):
418/// `name`/`kind` are filled by the runtime layer after the storage call.
419#[derive(Clone, Debug, Serialize, Deserialize)]
420pub struct PathNode {
421    #[serde(rename = "id")]
422    pub node_id: Uuid,
423    pub via_edge: Option<Uuid>,
424    pub depth: usize,
425    #[serde(default, skip_serializing_if = "Option::is_none")]
426    pub name: Option<String>,
427    #[serde(default, skip_serializing_if = "Option::is_none")]
428    pub kind: Option<String>,
429}
430
431#[derive(Clone, Debug, Serialize, Deserialize)]
432pub struct GraphPath {
433    pub root_id: Uuid,
434    pub nodes: Vec<PathNode>,
435    pub total_weight: f64,
436}