Skip to main content

post_cortex_storage/
traits.rs

1// Copyright (c) 2025, 2026 Julius ML
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in all
11// copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21//! Storage trait abstractions for post-cortex.
22//!
23//! This module defines the core storage traits that allow different backends
24//! (RocksDB, SurrealDB, etc.) to be used interchangeably.
25
26use crate::rocksdb_storage::{SessionCheckpoint, StoredWorkspace};
27use anyhow::Result;
28use async_trait::async_trait;
29use post_cortex_core::core::context_update::{
30    ContextUpdate, EntityData, EntityRelationship, RelationType,
31};
32use post_cortex_core::graph::entity_graph::EntityNetwork;
33use post_cortex_core::session::active_session::ActiveSession;
34use post_cortex_core::workspace::SessionRole;
35use post_cortex_embeddings::{SearchMatch, VectorMetadata};
36use post_cortex_proto::pb::{CascadeInvalidateReport, FreshnessEntry, SourceReference, SymbolId};
37use uuid::Uuid;
38
39/// Report returned when checking if sources are still fresh
40#[derive(Debug, Clone)]
41pub struct FreshnessReportExt {
42    /// Freshness entries, one per checked source.
43    pub entries: Vec<FreshnessEntry>,
44}
45
46/// A source_reference entry that is marked stale (status=1) — i.e. it was
47/// invalidated by `invalidate_source` or `cascade_invalidate` but has not yet
48/// been re-registered.  Returned by `get_stale_entries_by_source`.
49#[derive(Debug, Clone)]
50pub struct StaleEntryInfo {
51    /// Entry ID that was marked stale.
52    pub entry_id: String,
53    /// Symbol name associated with the entry (when known).
54    pub symbol_name: Option<String>,
55    /// Symbol type (e.g. `"fn"`, `"struct"`) associated with the entry.
56    pub symbol_type: Option<String>,
57}
58///
59/// This trait defines the fundamental storage operations required for
60/// session management, context updates, and workspace persistence.
61///
62/// Implementations must be Send + Sync for concurrent access.
63#[async_trait]
64pub trait Storage: FreshnessStorage + Send + Sync {
65    // ===== Session Operations =====
66
67    /// Save a session to storage
68    async fn save_session(&self, session: &ActiveSession) -> Result<()>;
69
70    /// Load a session from storage
71    async fn load_session(&self, session_id: Uuid) -> Result<ActiveSession>;
72
73    /// Delete a session and all related data
74    async fn delete_session(&self, session_id: Uuid) -> Result<()>;
75
76    /// Delete all entities and relationships for a session (used by entity graph rebuild)
77    async fn clear_session_entities(&self, session_id: Uuid) -> Result<()>;
78
79    /// List all session IDs
80    async fn list_sessions(&self) -> Result<Vec<Uuid>>;
81
82    /// Check if a session exists
83    async fn session_exists(&self, session_id: Uuid) -> Result<bool>;
84
85    // ===== Context Update Operations =====
86
87    /// Batch save multiple context updates efficiently
88    async fn batch_save_updates(&self, session_id: Uuid, updates: Vec<ContextUpdate>)
89    -> Result<()>;
90
91    /// Save session and context updates in a single atomic write.
92    /// Default implementation calls save_session then batch_save_updates sequentially.
93    /// Backends can override for a single WriteBatch operation.
94    async fn save_session_with_updates(
95        &self,
96        session: &ActiveSession,
97        session_id: Uuid,
98        updates: Vec<ContextUpdate>,
99    ) -> Result<()> {
100        self.save_session(session).await?;
101        if !updates.is_empty() {
102            self.batch_save_updates(session_id, updates).await?;
103        }
104        Ok(())
105    }
106
107    /// Load all updates for a session
108    async fn load_session_updates(&self, session_id: Uuid) -> Result<Vec<ContextUpdate>>;
109
110    // ===== Checkpoint Operations =====
111
112    /// Save a session checkpoint
113    async fn save_checkpoint(&self, checkpoint: &SessionCheckpoint) -> Result<()>;
114
115    /// Load a session checkpoint
116    async fn load_checkpoint(&self, checkpoint_id: Uuid) -> Result<SessionCheckpoint>;
117
118    /// List all checkpoints
119    async fn list_checkpoints(&self) -> Result<Vec<SessionCheckpoint>>;
120
121    // ===== Workspace Operations =====
122
123    /// Save workspace metadata
124    async fn save_workspace_metadata(
125        &self,
126        workspace_id: Uuid,
127        name: &str,
128        description: &str,
129        session_ids: &[Uuid],
130    ) -> Result<()>;
131
132    /// Delete a workspace
133    async fn delete_workspace(&self, workspace_id: Uuid) -> Result<()>;
134
135    /// List all workspaces
136    async fn list_workspaces(&self) -> Result<Vec<StoredWorkspace>>;
137
138    /// Add a session to a workspace
139    async fn add_session_to_workspace(
140        &self,
141        workspace_id: Uuid,
142        session_id: Uuid,
143        role: SessionRole,
144    ) -> Result<()>;
145
146    /// Remove a session from a workspace
147    async fn remove_session_from_workspace(
148        &self,
149        workspace_id: Uuid,
150        session_id: Uuid,
151    ) -> Result<()>;
152
153    // ===== Utility Operations =====
154
155    /// Force database compaction
156    async fn compact(&self) -> Result<()>;
157
158    /// Get estimated number of keys in database
159    async fn get_key_count(&self) -> Result<usize>;
160
161    /// Get database statistics as a string
162    async fn get_stats(&self) -> Result<String>;
163}
164
165/// Extended storage trait for graph-native operations.
166///
167/// This trait is implemented by backends with native graph support (e.g., SurrealDB).
168/// For backends without native graph support (e.g., RocksDB), graph operations
169/// are handled by the in-memory SimpleEntityGraph.
170#[async_trait]
171pub trait GraphStorage: Storage {
172    // ===== Entity Operations =====
173
174    /// Insert or update an entity
175    async fn upsert_entity(&self, session_id: Uuid, entity: &EntityData) -> Result<()>;
176
177    /// Get an entity by name
178    async fn get_entity(&self, session_id: Uuid, name: &str) -> Result<Option<EntityData>>;
179
180    /// List all entities for a session
181    async fn list_entities(&self, session_id: Uuid) -> Result<Vec<EntityData>>;
182
183    /// Delete an entity
184    async fn delete_entity(&self, session_id: Uuid, name: &str) -> Result<()>;
185
186    // ===== Relationship Operations =====
187
188    /// Create a relationship between entities
189    async fn create_relationship(
190        &self,
191        session_id: Uuid,
192        relationship: &EntityRelationship,
193    ) -> Result<()>;
194
195    /// Find all entities related to a given entity
196    async fn find_related_entities(
197        &self,
198        session_id: Uuid,
199        entity_name: &str,
200    ) -> Result<Vec<String>>;
201
202    /// Find entities related by a specific relation type
203    async fn find_related_by_type(
204        &self,
205        session_id: Uuid,
206        entity_name: &str,
207        relation_type: &RelationType,
208    ) -> Result<Vec<String>>;
209
210    /// Find the shortest path between two entities
211    async fn find_shortest_path(
212        &self,
213        session_id: Uuid,
214        from: &str,
215        to: &str,
216    ) -> Result<Option<Vec<String>>>;
217
218    /// Get the entity network (subgraph) around a center entity
219    async fn get_entity_network(
220        &self,
221        session_id: Uuid,
222        center: &str,
223        max_depth: usize,
224    ) -> Result<EntityNetwork>;
225}
226
227/// Vector storage trait for semantic search.
228///
229/// This trait can be implemented by:
230/// - SurrealDB (native HNSW)
231/// - VectorDB (in-memory)
232/// - External services (Qdrant, etc.)
233#[async_trait]
234pub trait VectorStorage: Send + Sync {
235    /// Add a vector with metadata
236    async fn add_vector(&self, vector: Vec<f32>, metadata: VectorMetadata) -> Result<String>;
237
238    /// Add multiple vectors in a batch
239    async fn add_vectors_batch(
240        &self,
241        vectors: Vec<(Vec<f32>, VectorMetadata)>,
242    ) -> Result<Vec<String>>;
243
244    /// Search for similar vectors
245    async fn search(&self, query: &[f32], k: usize) -> Result<Vec<SearchMatch>>;
246
247    /// Search within a specific session
248    async fn search_in_session(
249        &self,
250        query: &[f32],
251        k: usize,
252        session_id: &str,
253    ) -> Result<Vec<SearchMatch>>;
254
255    /// Search with a content type filter
256    async fn search_by_content_type(
257        &self,
258        query: &[f32],
259        k: usize,
260        content_type: &str,
261    ) -> Result<Vec<SearchMatch>>;
262
263    /// Remove a vector by ID
264    async fn remove_vector(&self, id: &str) -> Result<bool>;
265
266    /// Check if a session has any embeddings
267    async fn has_session_embeddings(&self, session_id: &str) -> bool;
268
269    /// Count embeddings for a session
270    async fn count_session_embeddings(&self, session_id: &str) -> usize;
271
272    /// Get total vector count
273    async fn total_count(&self) -> usize;
274
275    /// Get all vectors for a session (for loading into memory)
276    async fn get_session_vectors(
277        &self,
278        session_id: &str,
279    ) -> Result<Vec<(Vec<f32>, VectorMetadata)>>;
280
281    /// Get all vectors from storage (for loading into memory on startup)
282    async fn get_all_vectors(&self) -> Result<Vec<(Vec<f32>, VectorMetadata)>>;
283}
284
285/// Storage trait for source freshness tracking.
286///
287/// This trait manages tracking where information in PCX originally came from,
288/// allowing efficient invalidation when the source files change.
289#[async_trait]
290pub trait FreshnessStorage: Send + Sync {
291    /// Register or update a source reference for an entry
292    async fn register_source(&self, session_id: Uuid, reference: SourceReference) -> Result<()>;
293
294    /// Check if an entry's stored hash matches the provided hash.
295    /// When the stored entry has a FunctionScope with ast_hash, compares that
296    /// instead of the file-level hash (semantic freshness).
297    async fn check_freshness(&self, entry_id: &str, file_hash: &[u8]) -> Result<FreshnessEntry>;
298
299    /// Semantic freshness check: compare ast_hash when available, fallback to file hash.
300    /// The `ast_hash` and `symbol_name` are optional — if provided and the stored entry
301    /// has a FunctionScope, ast_hash comparison is used instead of file-level hash.
302    async fn check_freshness_semantic(
303        &self,
304        entry_id: &str,
305        file_hash: &[u8],
306        _ast_hash: Option<&[u8]>,
307        _symbol_name: Option<&str>,
308    ) -> Result<FreshnessEntry> {
309        // Default: delegate to basic check_freshness (backends override for semantic)
310        self.check_freshness(entry_id, file_hash).await
311    }
312
313    /// Invalidate (remove) a source reference, usually because the source changed
314    /// or the entry is being deleted. Returns count of removed references.
315    async fn invalidate_source(&self, file_path: &str) -> Result<u32>;
316
317    /// Get all entries associated with a specific file path
318    async fn get_entries_by_source(&self, file_path: &str) -> Result<Vec<SourceReference>>;
319
320    /// Return entry_ids (plus symbol metadata) for all source_reference records that
321    /// are still marked stale (status=1) for the given file.  Called by Axon after
322    /// `register_source_batch` to detect symbols that were deleted from the file
323    /// (they were marked stale by `invalidate_source` but never re-registered).
324    async fn get_stale_entries_by_source(&self, file_path: &str) -> Result<Vec<StaleEntryInfo>>;
325
326    /// Register symbol-level dependencies (e.g., fn foo depends on struct Bar).
327    /// Used for cascade invalidation.
328    async fn register_symbol_dependencies(
329        &self,
330        from: SymbolId,
331        to_symbols: Vec<SymbolId>,
332    ) -> Result<u32>;
333
334    /// Cascade invalidate: when a symbol changes, invalidate all entries
335    /// whose symbols depend on it (transitively up to max_depth).
336    async fn cascade_invalidate(
337        &self,
338        changed: SymbolId,
339        new_ast_hash: Vec<u8>,
340        max_depth: u32,
341    ) -> Result<CascadeInvalidateReport>;
342
343    /// Batch freshness check: check multiple entries in a single storage round-trip.
344    ///
345    /// Each element of `entries` is `(entry_id, file_hash, ast_hash, symbol_name)`.
346    /// Returns one `FreshnessEntry` per input element, in the same order.
347    ///
348    /// The default implementation loops over `check_freshness_semantic` calls.
349    /// Backends with native batch support (e.g., SurrealDB) should override this
350    /// with a single `WHERE entry_id IN $ids` query.
351    async fn check_freshness_batch(
352        &self,
353        entries: Vec<(String, Vec<u8>, Option<Vec<u8>>, Option<String>)>,
354    ) -> Result<Vec<FreshnessEntry>> {
355        let mut results = Vec::with_capacity(entries.len());
356        for (entry_id, file_hash, ast_hash, symbol_name) in entries {
357            let result = self
358                .check_freshness_semantic(
359                    &entry_id,
360                    &file_hash,
361                    ast_hash.as_deref(),
362                    symbol_name.as_deref(),
363                )
364                .await
365                .unwrap_or_else(|_| FreshnessEntry {
366                    entry_id: entry_id.clone(),
367                    file_path: String::new(),
368                    status: post_cortex_proto::pb::FreshnessStatus::Unknown as i32,
369                    stored_hash: Vec::new(),
370                    current_hash: file_hash,
371                });
372            results.push(result);
373        }
374        Ok(results)
375    }
376}
377
378/// Unified storage backend enum for runtime selection.
379///
380/// This enum wraps the different storage implementations and provides
381/// a unified interface for the rest of the application.
382#[derive(Clone)]
383pub enum StorageBackend {
384    /// RocksDB-based local storage (default)
385    RocksDB(crate::RealRocksDBStorage),
386
387    /// SurrealDB-based storage with native graph support
388    #[cfg(feature = "surrealdb-storage")]
389    SurrealDB(std::sync::Arc<crate::surrealdb_storage::SurrealDBStorage>),
390}
391
392impl StorageBackend {
393    /// Check if this backend supports native graph operations
394    pub fn supports_native_graph(&self) -> bool {
395        match self {
396            StorageBackend::RocksDB(_) => false,
397            #[cfg(feature = "surrealdb-storage")]
398            StorageBackend::SurrealDB(_) => true,
399        }
400    }
401
402    /// Check if this backend supports native vector operations
403    pub fn supports_native_vectors(&self) -> bool {
404        match self {
405            StorageBackend::RocksDB(_) => false,
406            #[cfg(feature = "surrealdb-storage")]
407            StorageBackend::SurrealDB(_) => true,
408        }
409    }
410}
411
412// Implement Storage trait for StorageBackend via delegation
413#[async_trait]
414impl Storage for StorageBackend {
415    async fn save_session(&self, session: &ActiveSession) -> Result<()> {
416        match self {
417            StorageBackend::RocksDB(storage) => storage.save_session(session).await,
418            #[cfg(feature = "surrealdb-storage")]
419            StorageBackend::SurrealDB(storage) => storage.save_session(session).await,
420        }
421    }
422
423    async fn load_session(&self, session_id: Uuid) -> Result<ActiveSession> {
424        match self {
425            StorageBackend::RocksDB(storage) => storage.load_session(session_id).await,
426            #[cfg(feature = "surrealdb-storage")]
427            StorageBackend::SurrealDB(storage) => storage.load_session(session_id).await,
428        }
429    }
430
431    async fn delete_session(&self, session_id: Uuid) -> Result<()> {
432        match self {
433            StorageBackend::RocksDB(storage) => storage.delete_session(session_id).await,
434            #[cfg(feature = "surrealdb-storage")]
435            StorageBackend::SurrealDB(storage) => storage.delete_session(session_id).await,
436        }
437    }
438
439    async fn clear_session_entities(&self, session_id: Uuid) -> Result<()> {
440        match self {
441            StorageBackend::RocksDB(storage) => storage.clear_session_entities(session_id).await,
442            #[cfg(feature = "surrealdb-storage")]
443            StorageBackend::SurrealDB(storage) => storage.clear_session_entities(session_id).await,
444        }
445    }
446
447    async fn list_sessions(&self) -> Result<Vec<Uuid>> {
448        match self {
449            StorageBackend::RocksDB(storage) => storage.list_sessions().await,
450            #[cfg(feature = "surrealdb-storage")]
451            StorageBackend::SurrealDB(storage) => storage.list_sessions().await,
452        }
453    }
454
455    async fn session_exists(&self, session_id: Uuid) -> Result<bool> {
456        match self {
457            StorageBackend::RocksDB(storage) => storage.session_exists(session_id).await,
458            #[cfg(feature = "surrealdb-storage")]
459            StorageBackend::SurrealDB(storage) => storage.session_exists(session_id).await,
460        }
461    }
462
463    async fn batch_save_updates(
464        &self,
465        session_id: Uuid,
466        updates: Vec<ContextUpdate>,
467    ) -> Result<()> {
468        match self {
469            StorageBackend::RocksDB(storage) => {
470                storage.batch_save_updates(session_id, updates).await
471            }
472            #[cfg(feature = "surrealdb-storage")]
473            StorageBackend::SurrealDB(storage) => {
474                storage.batch_save_updates(session_id, updates).await
475            }
476        }
477    }
478
479    async fn save_session_with_updates(
480        &self,
481        session: &ActiveSession,
482        session_id: Uuid,
483        updates: Vec<ContextUpdate>,
484    ) -> Result<()> {
485        match self {
486            StorageBackend::RocksDB(storage) => {
487                storage
488                    .save_session_with_updates(session, session_id, updates)
489                    .await
490            }
491            #[cfg(feature = "surrealdb-storage")]
492            StorageBackend::SurrealDB(_) => {
493                // Use default sequential implementation for SurrealDB
494                self.save_session(session).await?;
495                if !updates.is_empty() {
496                    self.batch_save_updates(session_id, updates).await?;
497                }
498                Ok(())
499            }
500        }
501    }
502
503    async fn load_session_updates(&self, session_id: Uuid) -> Result<Vec<ContextUpdate>> {
504        match self {
505            StorageBackend::RocksDB(storage) => storage.load_session_updates(session_id).await,
506            #[cfg(feature = "surrealdb-storage")]
507            StorageBackend::SurrealDB(storage) => storage.load_session_updates(session_id).await,
508        }
509    }
510
511    async fn save_checkpoint(&self, checkpoint: &SessionCheckpoint) -> Result<()> {
512        match self {
513            StorageBackend::RocksDB(storage) => storage.save_checkpoint(checkpoint).await,
514            #[cfg(feature = "surrealdb-storage")]
515            StorageBackend::SurrealDB(storage) => storage.save_checkpoint(checkpoint).await,
516        }
517    }
518
519    async fn load_checkpoint(&self, checkpoint_id: Uuid) -> Result<SessionCheckpoint> {
520        match self {
521            StorageBackend::RocksDB(storage) => storage.load_checkpoint(checkpoint_id).await,
522            #[cfg(feature = "surrealdb-storage")]
523            StorageBackend::SurrealDB(storage) => storage.load_checkpoint(checkpoint_id).await,
524        }
525    }
526
527    async fn list_checkpoints(&self) -> Result<Vec<SessionCheckpoint>> {
528        match self {
529            StorageBackend::RocksDB(storage) => storage.list_checkpoints().await,
530            #[cfg(feature = "surrealdb-storage")]
531            StorageBackend::SurrealDB(storage) => storage.list_checkpoints().await,
532        }
533    }
534
535    async fn save_workspace_metadata(
536        &self,
537        workspace_id: Uuid,
538        name: &str,
539        description: &str,
540        session_ids: &[Uuid],
541    ) -> Result<()> {
542        match self {
543            StorageBackend::RocksDB(storage) => {
544                storage
545                    .save_workspace_metadata(workspace_id, name, description, session_ids)
546                    .await
547            }
548            #[cfg(feature = "surrealdb-storage")]
549            StorageBackend::SurrealDB(storage) => {
550                storage
551                    .save_workspace_metadata(workspace_id, name, description, session_ids)
552                    .await
553            }
554        }
555    }
556
557    async fn delete_workspace(&self, workspace_id: Uuid) -> Result<()> {
558        match self {
559            StorageBackend::RocksDB(storage) => storage.delete_workspace(workspace_id).await,
560            #[cfg(feature = "surrealdb-storage")]
561            StorageBackend::SurrealDB(storage) => storage.delete_workspace(workspace_id).await,
562        }
563    }
564
565    async fn list_workspaces(&self) -> Result<Vec<StoredWorkspace>> {
566        match self {
567            StorageBackend::RocksDB(storage) => storage.list_workspaces().await,
568            #[cfg(feature = "surrealdb-storage")]
569            StorageBackend::SurrealDB(storage) => storage.list_workspaces().await,
570        }
571    }
572
573    async fn add_session_to_workspace(
574        &self,
575        workspace_id: Uuid,
576        session_id: Uuid,
577        role: SessionRole,
578    ) -> Result<()> {
579        match self {
580            StorageBackend::RocksDB(storage) => {
581                storage
582                    .add_session_to_workspace(workspace_id, session_id, role)
583                    .await
584            }
585            #[cfg(feature = "surrealdb-storage")]
586            StorageBackend::SurrealDB(storage) => {
587                storage
588                    .add_session_to_workspace(workspace_id, session_id, role)
589                    .await
590            }
591        }
592    }
593
594    async fn remove_session_from_workspace(
595        &self,
596        workspace_id: Uuid,
597        session_id: Uuid,
598    ) -> Result<()> {
599        match self {
600            StorageBackend::RocksDB(storage) => {
601                storage
602                    .remove_session_from_workspace(workspace_id, session_id)
603                    .await
604            }
605            #[cfg(feature = "surrealdb-storage")]
606            StorageBackend::SurrealDB(storage) => {
607                storage
608                    .remove_session_from_workspace(workspace_id, session_id)
609                    .await
610            }
611        }
612    }
613
614    async fn compact(&self) -> Result<()> {
615        match self {
616            StorageBackend::RocksDB(storage) => storage.compact().await,
617            #[cfg(feature = "surrealdb-storage")]
618            StorageBackend::SurrealDB(storage) => storage.compact().await,
619        }
620    }
621
622    async fn get_key_count(&self) -> Result<usize> {
623        match self {
624            StorageBackend::RocksDB(storage) => storage.get_key_count().await,
625            #[cfg(feature = "surrealdb-storage")]
626            StorageBackend::SurrealDB(storage) => storage.get_key_count().await,
627        }
628    }
629
630    async fn get_stats(&self) -> Result<String> {
631        match self {
632            StorageBackend::RocksDB(storage) => storage.get_stats().await,
633            #[cfg(feature = "surrealdb-storage")]
634            StorageBackend::SurrealDB(storage) => storage.get_stats().await,
635        }
636    }
637}
638
639// Implement GraphStorage trait for StorageBackend via delegation
640#[async_trait]
641impl GraphStorage for StorageBackend {
642    async fn upsert_entity(&self, session_id: Uuid, entity: &EntityData) -> Result<()> {
643        match self {
644            StorageBackend::RocksDB(storage) => storage.upsert_entity(session_id, entity).await,
645            #[cfg(feature = "surrealdb-storage")]
646            StorageBackend::SurrealDB(storage) => storage.upsert_entity(session_id, entity).await,
647        }
648    }
649
650    async fn get_entity(&self, session_id: Uuid, name: &str) -> Result<Option<EntityData>> {
651        match self {
652            StorageBackend::RocksDB(storage) => storage.get_entity(session_id, name).await,
653            #[cfg(feature = "surrealdb-storage")]
654            StorageBackend::SurrealDB(storage) => storage.get_entity(session_id, name).await,
655        }
656    }
657
658    async fn list_entities(&self, session_id: Uuid) -> Result<Vec<EntityData>> {
659        match self {
660            StorageBackend::RocksDB(storage) => storage.list_entities(session_id).await,
661            #[cfg(feature = "surrealdb-storage")]
662            StorageBackend::SurrealDB(storage) => storage.list_entities(session_id).await,
663        }
664    }
665
666    async fn delete_entity(&self, session_id: Uuid, name: &str) -> Result<()> {
667        match self {
668            StorageBackend::RocksDB(storage) => storage.delete_entity(session_id, name).await,
669            #[cfg(feature = "surrealdb-storage")]
670            StorageBackend::SurrealDB(storage) => storage.delete_entity(session_id, name).await,
671        }
672    }
673
674    async fn create_relationship(
675        &self,
676        session_id: Uuid,
677        relationship: &EntityRelationship,
678    ) -> Result<()> {
679        match self {
680            StorageBackend::RocksDB(storage) => {
681                storage.create_relationship(session_id, relationship).await
682            }
683            #[cfg(feature = "surrealdb-storage")]
684            StorageBackend::SurrealDB(storage) => {
685                storage.create_relationship(session_id, relationship).await
686            }
687        }
688    }
689
690    async fn find_related_entities(
691        &self,
692        session_id: Uuid,
693        entity_name: &str,
694    ) -> Result<Vec<String>> {
695        match self {
696            StorageBackend::RocksDB(storage) => {
697                storage.find_related_entities(session_id, entity_name).await
698            }
699            #[cfg(feature = "surrealdb-storage")]
700            StorageBackend::SurrealDB(storage) => {
701                storage.find_related_entities(session_id, entity_name).await
702            }
703        }
704    }
705
706    async fn find_related_by_type(
707        &self,
708        session_id: Uuid,
709        entity_name: &str,
710        relation_type: &RelationType,
711    ) -> Result<Vec<String>> {
712        match self {
713            StorageBackend::RocksDB(storage) => {
714                storage
715                    .find_related_by_type(session_id, entity_name, relation_type)
716                    .await
717            }
718            #[cfg(feature = "surrealdb-storage")]
719            StorageBackend::SurrealDB(storage) => {
720                storage
721                    .find_related_by_type(session_id, entity_name, relation_type)
722                    .await
723            }
724        }
725    }
726
727    async fn find_shortest_path(
728        &self,
729        session_id: Uuid,
730        from: &str,
731        to: &str,
732    ) -> Result<Option<Vec<String>>> {
733        match self {
734            StorageBackend::RocksDB(storage) => {
735                storage.find_shortest_path(session_id, from, to).await
736            }
737            #[cfg(feature = "surrealdb-storage")]
738            StorageBackend::SurrealDB(storage) => {
739                storage.find_shortest_path(session_id, from, to).await
740            }
741        }
742    }
743
744    async fn get_entity_network(
745        &self,
746        session_id: Uuid,
747        center: &str,
748        max_depth: usize,
749    ) -> Result<EntityNetwork> {
750        match self {
751            StorageBackend::RocksDB(storage) => {
752                storage
753                    .get_entity_network(session_id, center, max_depth)
754                    .await
755            }
756            #[cfg(feature = "surrealdb-storage")]
757            StorageBackend::SurrealDB(storage) => {
758                storage
759                    .get_entity_network(session_id, center, max_depth)
760                    .await
761            }
762        }
763    }
764}
765
766// Implement FreshnessStorage trait for StorageBackend via delegation
767#[async_trait]
768impl FreshnessStorage for StorageBackend {
769    async fn register_source(&self, session_id: Uuid, reference: SourceReference) -> Result<()> {
770        match self {
771            StorageBackend::RocksDB(storage) => {
772                storage.register_source(session_id, reference).await
773            }
774            #[cfg(feature = "surrealdb-storage")]
775            StorageBackend::SurrealDB(storage) => {
776                storage.register_source(session_id, reference).await
777            }
778        }
779    }
780
781    async fn check_freshness(&self, entry_id: &str, file_hash: &[u8]) -> Result<FreshnessEntry> {
782        match self {
783            StorageBackend::RocksDB(storage) => storage.check_freshness(entry_id, file_hash).await,
784            #[cfg(feature = "surrealdb-storage")]
785            StorageBackend::SurrealDB(storage) => {
786                storage.check_freshness(entry_id, file_hash).await
787            }
788        }
789    }
790
791    async fn invalidate_source(&self, file_path: &str) -> Result<u32> {
792        match self {
793            StorageBackend::RocksDB(storage) => storage.invalidate_source(file_path).await,
794            #[cfg(feature = "surrealdb-storage")]
795            StorageBackend::SurrealDB(storage) => storage.invalidate_source(file_path).await,
796        }
797    }
798
799    async fn get_entries_by_source(&self, file_path: &str) -> Result<Vec<SourceReference>> {
800        match self {
801            StorageBackend::RocksDB(storage) => storage.get_entries_by_source(file_path).await,
802            #[cfg(feature = "surrealdb-storage")]
803            StorageBackend::SurrealDB(storage) => storage.get_entries_by_source(file_path).await,
804        }
805    }
806
807    async fn get_stale_entries_by_source(&self, file_path: &str) -> Result<Vec<StaleEntryInfo>> {
808        match self {
809            StorageBackend::RocksDB(storage) => {
810                storage.get_stale_entries_by_source(file_path).await
811            }
812            #[cfg(feature = "surrealdb-storage")]
813            StorageBackend::SurrealDB(storage) => {
814                storage.get_stale_entries_by_source(file_path).await
815            }
816        }
817    }
818
819    async fn check_freshness_semantic(
820        &self,
821        entry_id: &str,
822        file_hash: &[u8],
823        ast_hash: Option<&[u8]>,
824        symbol_name: Option<&str>,
825    ) -> Result<FreshnessEntry> {
826        match self {
827            StorageBackend::RocksDB(storage) => {
828                storage
829                    .check_freshness_semantic(entry_id, file_hash, ast_hash, symbol_name)
830                    .await
831            }
832            #[cfg(feature = "surrealdb-storage")]
833            StorageBackend::SurrealDB(storage) => {
834                storage
835                    .check_freshness_semantic(entry_id, file_hash, ast_hash, symbol_name)
836                    .await
837            }
838        }
839    }
840
841    async fn register_symbol_dependencies(
842        &self,
843        from: SymbolId,
844        to_symbols: Vec<SymbolId>,
845    ) -> Result<u32> {
846        match self {
847            StorageBackend::RocksDB(storage) => {
848                storage.register_symbol_dependencies(from, to_symbols).await
849            }
850            #[cfg(feature = "surrealdb-storage")]
851            StorageBackend::SurrealDB(storage) => {
852                storage.register_symbol_dependencies(from, to_symbols).await
853            }
854        }
855    }
856
857    async fn cascade_invalidate(
858        &self,
859        changed: SymbolId,
860        new_ast_hash: Vec<u8>,
861        max_depth: u32,
862    ) -> Result<CascadeInvalidateReport> {
863        match self {
864            StorageBackend::RocksDB(storage) => {
865                storage
866                    .cascade_invalidate(changed, new_ast_hash, max_depth)
867                    .await
868            }
869            #[cfg(feature = "surrealdb-storage")]
870            StorageBackend::SurrealDB(storage) => {
871                storage
872                    .cascade_invalidate(changed, new_ast_hash, max_depth)
873                    .await
874            }
875        }
876    }
877
878    async fn check_freshness_batch(
879        &self,
880        entries: Vec<(String, Vec<u8>, Option<Vec<u8>>, Option<String>)>,
881    ) -> Result<Vec<FreshnessEntry>> {
882        match self {
883            StorageBackend::RocksDB(storage) => storage.check_freshness_batch(entries).await,
884            #[cfg(feature = "surrealdb-storage")]
885            StorageBackend::SurrealDB(storage) => storage.check_freshness_batch(entries).await,
886        }
887    }
888}
889
890/// Storage configuration for backend selection
891#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
892pub struct StorageConfig {
893    /// Storage backend type
894    pub backend: StorageBackendType,
895
896    /// Path to storage data directory
897    pub path: std::path::PathBuf,
898
899    /// SurrealDB-specific configuration
900    #[cfg(feature = "surrealdb-storage")]
901    pub surrealdb: Option<SurrealDBConfig>,
902}
903
904/// Storage backend type
905#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
906#[serde(rename_all = "lowercase")]
907#[derive(Default)]
908pub enum StorageBackendType {
909    /// RocksDB local storage (default)
910    #[default]
911    RocksDB,
912
913    /// SurrealDB storage with native graph support
914    #[cfg(feature = "surrealdb-storage")]
915    SurrealDB,
916}
917
918impl StorageBackendType {
919    /// Parse storage backend type from string
920    pub fn from_str(s: &str) -> Option<Self> {
921        match s.to_lowercase().as_str() {
922            "rocksdb" | "rocks" => Some(StorageBackendType::RocksDB),
923            #[cfg(feature = "surrealdb-storage")]
924            "surrealdb" | "surreal" => Some(StorageBackendType::SurrealDB),
925            _ => None,
926        }
927    }
928}
929
930/// SurrealDB-specific configuration
931#[cfg(feature = "surrealdb-storage")]
932#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
933pub struct SurrealDBConfig {
934    /// SurrealDB namespace
935    pub namespace: String,
936
937    /// SurrealDB database name
938    pub database: String,
939
940    /// TiKV endpoints (for distributed mode)
941    pub tikv_endpoints: Option<Vec<String>>,
942}
943
944#[cfg(feature = "surrealdb-storage")]
945impl Default for SurrealDBConfig {
946    fn default() -> Self {
947        Self {
948            namespace: "post_cortex".to_string(),
949            database: "main".to_string(),
950            tikv_endpoints: None,
951        }
952    }
953}
954
955impl Default for StorageConfig {
956    fn default() -> Self {
957        Self {
958            backend: StorageBackendType::default(),
959            path: dirs::data_local_dir()
960                .unwrap_or_else(|| std::path::PathBuf::from("."))
961                .join("post-cortex")
962                .join("data"),
963            #[cfg(feature = "surrealdb-storage")]
964            surrealdb: None,
965        }
966    }
967}