khive_storage/entity.rs
1//! Entity storage capability — graph node CRUD.
2
3use async_trait::async_trait;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use uuid::Uuid;
7
8use crate::types::{BatchWriteSummary, DeleteMode, Page, PageRequest, StorageResult};
9
10/// Storage-level entity record. Flat SQL-friendly representation.
11/// Maps to the `entities` substrate table.
12#[derive(Clone, Debug, Serialize, Deserialize)]
13pub struct Entity {
14 pub id: Uuid,
15 pub namespace: String,
16 pub kind: String,
17 /// Pack-governed subtype token. Maps to `entities.entity_type` column.
18 pub entity_type: Option<String>,
19 pub name: String,
20 pub description: Option<String>,
21 pub properties: Option<Value>,
22 pub tags: Vec<String>,
23 pub created_at: i64,
24 pub updated_at: i64,
25 pub deleted_at: Option<i64>,
26 /// When this entity was tombstoned by a merge, the `into` entity's ID.
27 pub merged_into: Option<Uuid>,
28 /// Opaque event ID for the merge that tombstoned this entity.
29 pub merge_event_id: Option<Uuid>,
30}
31
32impl Entity {
33 /// Create a new entity with a generated UUID and current timestamp.
34 pub fn new(
35 namespace: impl Into<String>,
36 kind: impl Into<String>,
37 name: impl Into<String>,
38 ) -> Self {
39 let now = chrono::Utc::now().timestamp_micros();
40 Self {
41 id: Uuid::new_v4(),
42 namespace: namespace.into(),
43 kind: kind.into(),
44 entity_type: None,
45 name: name.into(),
46 description: None,
47 properties: None,
48 tags: Vec::new(),
49 created_at: now,
50 updated_at: now,
51 deleted_at: None,
52 merged_into: None,
53 merge_event_id: None,
54 }
55 }
56
57 /// Set the pack-governed entity subtype token.
58 pub fn with_entity_type(mut self, t: Option<impl Into<String>>) -> Self {
59 self.entity_type = t.map(Into::into);
60 self
61 }
62
63 /// Set the entity description.
64 pub fn with_description(mut self, d: impl Into<String>) -> Self {
65 self.description = Some(d.into());
66 self
67 }
68
69 /// Set the entity properties JSON blob.
70 pub fn with_properties(mut self, p: Value) -> Self {
71 self.properties = Some(p);
72 self
73 }
74
75 /// Set the entity tags.
76 pub fn with_tags(mut self, t: Vec<String>) -> Self {
77 self.tags = t;
78 self
79 }
80}
81
82/// Entity filter for query operations.
83#[derive(Clone, Debug, Default, Serialize, Deserialize)]
84pub struct EntityFilter {
85 pub ids: Vec<Uuid>,
86 pub kinds: Vec<String>,
87 /// Filter by exact `entity_type` value. Multiple values are ORed.
88 pub entity_types: Vec<String>,
89 pub name_prefix: Option<String>,
90 /// Deterministic, case-sensitive equality on `entities.name` (binary
91 /// comparison — SQLite's default collation for `=` on a `TEXT` column
92 /// without an explicit `COLLATE NOCASE`). Distinct from `name_prefix`:
93 /// that stage's `LIKE` is inherently prefix-shaped and, with SQLite's
94 /// default `NOCASE`-free `LIKE` on ASCII, still ranks a page by
95 /// `created_at DESC` — a match that is exact but not the newest can be
96 /// paged out. `name_exact` skips paging risk entirely by filtering to
97 /// only rows that equal `name` at the SQL layer.
98 pub name_exact: Option<String>,
99 pub tags_any: Vec<String>,
100 /// When non-empty, restricts results to any of these namespaces using
101 /// `namespace IN (...)`. Takes precedence over the `namespace` string
102 /// parameter passed to `query_entities` / `count_entities`. When empty the
103 /// caller-supplied `namespace` parameter is used (single-namespace path,
104 /// backward-compatible default).
105 #[serde(default)]
106 pub namespaces: Vec<String>,
107}
108
109/// Entity CRUD operations over the entities substrate table.
110#[async_trait]
111pub trait EntityStore: Send + Sync + 'static {
112 /// Insert or update a single entity.
113 async fn upsert_entity(&self, entity: Entity) -> StorageResult<()>;
114 /// Insert or update a batch of entities.
115 async fn upsert_entities(&self, entities: Vec<Entity>) -> StorageResult<BatchWriteSummary>;
116 /// Fetch an entity by UUID, returning `None` if absent.
117 async fn get_entity(&self, id: Uuid) -> StorageResult<Option<Entity>>;
118 /// Delete an entity by UUID using the specified delete mode.
119 async fn delete_entity(&self, id: Uuid, mode: DeleteMode) -> StorageResult<bool>;
120 /// Query entities by namespace with filter and pagination.
121 async fn query_entities(
122 &self,
123 namespace: &str,
124 filter: EntityFilter,
125 page: PageRequest,
126 ) -> StorageResult<Page<Entity>>;
127 /// Count entities in a namespace matching the given filter.
128 async fn count_entities(&self, namespace: &str, filter: EntityFilter) -> StorageResult<u64>;
129 /// Fetch an entity by UUID regardless of soft-deletion state.
130 ///
131 /// Returns the entity row even when `deleted_at` is set. Callers use this
132 /// to distinguish "soft-deleted" from "never existed".
133 async fn get_entity_including_deleted(&self, id: Uuid) -> StorageResult<Option<Entity>>;
134}