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 post_cortex_core::core::context_update::{ContextUpdate, EntityData, EntityRelationship, RelationType};
27use post_cortex_embeddings::{SearchMatch, VectorMetadata};
28use post_cortex_proto::pb::{
29    CascadeInvalidateReport, FreshnessEntry, SourceReference, SymbolId,
30};
31use post_cortex_core::graph::entity_graph::EntityNetwork;
32use post_cortex_core::session::active_session::ActiveSession;
33use crate::rocksdb_storage::{SessionCheckpoint, StoredWorkspace};
34use post_cortex_core::workspace::SessionRole;
35use anyhow::Result;
36use async_trait::async_trait;
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(
325        &self,
326        file_path: &str,
327    ) -> Result<Vec<StaleEntryInfo>>;
328
329    /// Register symbol-level dependencies (e.g., fn foo depends on struct Bar).
330    /// Used for cascade invalidation.
331    async fn register_symbol_dependencies(
332        &self,
333        from: SymbolId,
334        to_symbols: Vec<SymbolId>,
335    ) -> Result<u32>;
336
337    /// Cascade invalidate: when a symbol changes, invalidate all entries
338    /// whose symbols depend on it (transitively up to max_depth).
339    async fn cascade_invalidate(
340        &self,
341        changed: SymbolId,
342        new_ast_hash: Vec<u8>,
343        max_depth: u32,
344    ) -> Result<CascadeInvalidateReport>;
345
346    /// Batch freshness check: check multiple entries in a single storage round-trip.
347    ///
348    /// Each element of `entries` is `(entry_id, file_hash, ast_hash, symbol_name)`.
349    /// Returns one `FreshnessEntry` per input element, in the same order.
350    ///
351    /// The default implementation loops over `check_freshness_semantic` calls.
352    /// Backends with native batch support (e.g., SurrealDB) should override this
353    /// with a single `WHERE entry_id IN $ids` query.
354    async fn check_freshness_batch(
355        &self,
356        entries: Vec<(String, Vec<u8>, Option<Vec<u8>>, Option<String>)>,
357    ) -> Result<Vec<FreshnessEntry>> {
358        let mut results = Vec::with_capacity(entries.len());
359        for (entry_id, file_hash, ast_hash, symbol_name) in entries {
360            let result = self
361                .check_freshness_semantic(
362                    &entry_id,
363                    &file_hash,
364                    ast_hash.as_deref(),
365                    symbol_name.as_deref(),
366                )
367                .await
368                .unwrap_or_else(|_| FreshnessEntry {
369                    entry_id: entry_id.clone(),
370                    file_path: String::new(),
371                    status: post_cortex_proto::pb::FreshnessStatus::Unknown as i32,
372                    stored_hash: Vec::new(),
373                    current_hash: file_hash,
374                });
375            results.push(result);
376        }
377        Ok(results)
378    }
379}
380
381/// Unified storage backend enum for runtime selection.
382///
383/// This enum wraps the different storage implementations and provides
384/// a unified interface for the rest of the application.
385#[derive(Clone)]
386pub enum StorageBackend {
387    /// RocksDB-based local storage (default)
388    RocksDB(crate::RealRocksDBStorage),
389
390    /// SurrealDB-based storage with native graph support
391    #[cfg(feature = "surrealdb-storage")]
392    SurrealDB(std::sync::Arc<crate::surrealdb_storage::SurrealDBStorage>),
393}
394
395impl StorageBackend {
396    /// Check if this backend supports native graph operations
397    pub fn supports_native_graph(&self) -> bool {
398        match self {
399            StorageBackend::RocksDB(_) => false,
400            #[cfg(feature = "surrealdb-storage")]
401            StorageBackend::SurrealDB(_) => true,
402        }
403    }
404
405    /// Check if this backend supports native vector operations
406    pub fn supports_native_vectors(&self) -> bool {
407        match self {
408            StorageBackend::RocksDB(_) => false,
409            #[cfg(feature = "surrealdb-storage")]
410            StorageBackend::SurrealDB(_) => true,
411        }
412    }
413}
414
415// Implement Storage trait for StorageBackend via delegation
416#[async_trait]
417impl Storage for StorageBackend {
418    async fn save_session(&self, session: &ActiveSession) -> Result<()> {
419        match self {
420            StorageBackend::RocksDB(storage) => storage.save_session(session).await,
421            #[cfg(feature = "surrealdb-storage")]
422            StorageBackend::SurrealDB(storage) => storage.save_session(session).await,
423        }
424    }
425
426    async fn load_session(&self, session_id: Uuid) -> Result<ActiveSession> {
427        match self {
428            StorageBackend::RocksDB(storage) => storage.load_session(session_id).await,
429            #[cfg(feature = "surrealdb-storage")]
430            StorageBackend::SurrealDB(storage) => storage.load_session(session_id).await,
431        }
432    }
433
434    async fn delete_session(&self, session_id: Uuid) -> Result<()> {
435        match self {
436            StorageBackend::RocksDB(storage) => storage.delete_session(session_id).await,
437            #[cfg(feature = "surrealdb-storage")]
438            StorageBackend::SurrealDB(storage) => storage.delete_session(session_id).await,
439        }
440    }
441
442    async fn clear_session_entities(&self, session_id: Uuid) -> Result<()> {
443        match self {
444            StorageBackend::RocksDB(storage) => storage.clear_session_entities(session_id).await,
445            #[cfg(feature = "surrealdb-storage")]
446            StorageBackend::SurrealDB(storage) => storage.clear_session_entities(session_id).await,
447        }
448    }
449
450    async fn list_sessions(&self) -> Result<Vec<Uuid>> {
451        match self {
452            StorageBackend::RocksDB(storage) => storage.list_sessions().await,
453            #[cfg(feature = "surrealdb-storage")]
454            StorageBackend::SurrealDB(storage) => storage.list_sessions().await,
455        }
456    }
457
458    async fn session_exists(&self, session_id: Uuid) -> Result<bool> {
459        match self {
460            StorageBackend::RocksDB(storage) => storage.session_exists(session_id).await,
461            #[cfg(feature = "surrealdb-storage")]
462            StorageBackend::SurrealDB(storage) => storage.session_exists(session_id).await,
463        }
464    }
465
466    async fn batch_save_updates(
467        &self,
468        session_id: Uuid,
469        updates: Vec<ContextUpdate>,
470    ) -> Result<()> {
471        match self {
472            StorageBackend::RocksDB(storage) => {
473                storage.batch_save_updates(session_id, updates).await
474            }
475            #[cfg(feature = "surrealdb-storage")]
476            StorageBackend::SurrealDB(storage) => {
477                storage.batch_save_updates(session_id, updates).await
478            }
479        }
480    }
481
482    async fn save_session_with_updates(
483        &self,
484        session: &ActiveSession,
485        session_id: Uuid,
486        updates: Vec<ContextUpdate>,
487    ) -> Result<()> {
488        match self {
489            StorageBackend::RocksDB(storage) => {
490                storage
491                    .save_session_with_updates(session, session_id, updates)
492                    .await
493            }
494            #[cfg(feature = "surrealdb-storage")]
495            StorageBackend::SurrealDB(_) => {
496                // Use default sequential implementation for SurrealDB
497                self.save_session(session).await?;
498                if !updates.is_empty() {
499                    self.batch_save_updates(session_id, updates).await?;
500                }
501                Ok(())
502            }
503        }
504    }
505
506    async fn load_session_updates(&self, session_id: Uuid) -> Result<Vec<ContextUpdate>> {
507        match self {
508            StorageBackend::RocksDB(storage) => storage.load_session_updates(session_id).await,
509            #[cfg(feature = "surrealdb-storage")]
510            StorageBackend::SurrealDB(storage) => storage.load_session_updates(session_id).await,
511        }
512    }
513
514    async fn save_checkpoint(&self, checkpoint: &SessionCheckpoint) -> Result<()> {
515        match self {
516            StorageBackend::RocksDB(storage) => storage.save_checkpoint(checkpoint).await,
517            #[cfg(feature = "surrealdb-storage")]
518            StorageBackend::SurrealDB(storage) => storage.save_checkpoint(checkpoint).await,
519        }
520    }
521
522    async fn load_checkpoint(&self, checkpoint_id: Uuid) -> Result<SessionCheckpoint> {
523        match self {
524            StorageBackend::RocksDB(storage) => storage.load_checkpoint(checkpoint_id).await,
525            #[cfg(feature = "surrealdb-storage")]
526            StorageBackend::SurrealDB(storage) => storage.load_checkpoint(checkpoint_id).await,
527        }
528    }
529
530    async fn list_checkpoints(&self) -> Result<Vec<SessionCheckpoint>> {
531        match self {
532            StorageBackend::RocksDB(storage) => storage.list_checkpoints().await,
533            #[cfg(feature = "surrealdb-storage")]
534            StorageBackend::SurrealDB(storage) => storage.list_checkpoints().await,
535        }
536    }
537
538    async fn save_workspace_metadata(
539        &self,
540        workspace_id: Uuid,
541        name: &str,
542        description: &str,
543        session_ids: &[Uuid],
544    ) -> Result<()> {
545        match self {
546            StorageBackend::RocksDB(storage) => {
547                storage
548                    .save_workspace_metadata(workspace_id, name, description, session_ids)
549                    .await
550            }
551            #[cfg(feature = "surrealdb-storage")]
552            StorageBackend::SurrealDB(storage) => {
553                storage
554                    .save_workspace_metadata(workspace_id, name, description, session_ids)
555                    .await
556            }
557        }
558    }
559
560    async fn delete_workspace(&self, workspace_id: Uuid) -> Result<()> {
561        match self {
562            StorageBackend::RocksDB(storage) => storage.delete_workspace(workspace_id).await,
563            #[cfg(feature = "surrealdb-storage")]
564            StorageBackend::SurrealDB(storage) => storage.delete_workspace(workspace_id).await,
565        }
566    }
567
568    async fn list_workspaces(&self) -> Result<Vec<StoredWorkspace>> {
569        match self {
570            StorageBackend::RocksDB(storage) => storage.list_workspaces().await,
571            #[cfg(feature = "surrealdb-storage")]
572            StorageBackend::SurrealDB(storage) => storage.list_workspaces().await,
573        }
574    }
575
576    async fn add_session_to_workspace(
577        &self,
578        workspace_id: Uuid,
579        session_id: Uuid,
580        role: SessionRole,
581    ) -> Result<()> {
582        match self {
583            StorageBackend::RocksDB(storage) => {
584                storage
585                    .add_session_to_workspace(workspace_id, session_id, role)
586                    .await
587            }
588            #[cfg(feature = "surrealdb-storage")]
589            StorageBackend::SurrealDB(storage) => {
590                storage
591                    .add_session_to_workspace(workspace_id, session_id, role)
592                    .await
593            }
594        }
595    }
596
597    async fn remove_session_from_workspace(
598        &self,
599        workspace_id: Uuid,
600        session_id: Uuid,
601    ) -> Result<()> {
602        match self {
603            StorageBackend::RocksDB(storage) => {
604                storage
605                    .remove_session_from_workspace(workspace_id, session_id)
606                    .await
607            }
608            #[cfg(feature = "surrealdb-storage")]
609            StorageBackend::SurrealDB(storage) => {
610                storage
611                    .remove_session_from_workspace(workspace_id, session_id)
612                    .await
613            }
614        }
615    }
616
617    async fn compact(&self) -> Result<()> {
618        match self {
619            StorageBackend::RocksDB(storage) => storage.compact().await,
620            #[cfg(feature = "surrealdb-storage")]
621            StorageBackend::SurrealDB(storage) => storage.compact().await,
622        }
623    }
624
625    async fn get_key_count(&self) -> Result<usize> {
626        match self {
627            StorageBackend::RocksDB(storage) => storage.get_key_count().await,
628            #[cfg(feature = "surrealdb-storage")]
629            StorageBackend::SurrealDB(storage) => storage.get_key_count().await,
630        }
631    }
632
633    async fn get_stats(&self) -> Result<String> {
634        match self {
635            StorageBackend::RocksDB(storage) => storage.get_stats().await,
636            #[cfg(feature = "surrealdb-storage")]
637            StorageBackend::SurrealDB(storage) => storage.get_stats().await,
638        }
639    }
640}
641
642// Implement GraphStorage trait for StorageBackend via delegation
643#[async_trait]
644impl GraphStorage for StorageBackend {
645    async fn upsert_entity(&self, session_id: Uuid, entity: &EntityData) -> Result<()> {
646        match self {
647            StorageBackend::RocksDB(storage) => storage.upsert_entity(session_id, entity).await,
648            #[cfg(feature = "surrealdb-storage")]
649            StorageBackend::SurrealDB(storage) => storage.upsert_entity(session_id, entity).await,
650        }
651    }
652
653    async fn get_entity(&self, session_id: Uuid, name: &str) -> Result<Option<EntityData>> {
654        match self {
655            StorageBackend::RocksDB(storage) => storage.get_entity(session_id, name).await,
656            #[cfg(feature = "surrealdb-storage")]
657            StorageBackend::SurrealDB(storage) => storage.get_entity(session_id, name).await,
658        }
659    }
660
661    async fn list_entities(&self, session_id: Uuid) -> Result<Vec<EntityData>> {
662        match self {
663            StorageBackend::RocksDB(storage) => storage.list_entities(session_id).await,
664            #[cfg(feature = "surrealdb-storage")]
665            StorageBackend::SurrealDB(storage) => storage.list_entities(session_id).await,
666        }
667    }
668
669    async fn delete_entity(&self, session_id: Uuid, name: &str) -> Result<()> {
670        match self {
671            StorageBackend::RocksDB(storage) => storage.delete_entity(session_id, name).await,
672            #[cfg(feature = "surrealdb-storage")]
673            StorageBackend::SurrealDB(storage) => storage.delete_entity(session_id, name).await,
674        }
675    }
676
677    async fn create_relationship(
678        &self,
679        session_id: Uuid,
680        relationship: &EntityRelationship,
681    ) -> Result<()> {
682        match self {
683            StorageBackend::RocksDB(storage) => {
684                storage.create_relationship(session_id, relationship).await
685            }
686            #[cfg(feature = "surrealdb-storage")]
687            StorageBackend::SurrealDB(storage) => {
688                storage.create_relationship(session_id, relationship).await
689            }
690        }
691    }
692
693    async fn find_related_entities(
694        &self,
695        session_id: Uuid,
696        entity_name: &str,
697    ) -> Result<Vec<String>> {
698        match self {
699            StorageBackend::RocksDB(storage) => {
700                storage.find_related_entities(session_id, entity_name).await
701            }
702            #[cfg(feature = "surrealdb-storage")]
703            StorageBackend::SurrealDB(storage) => {
704                storage.find_related_entities(session_id, entity_name).await
705            }
706        }
707    }
708
709    async fn find_related_by_type(
710        &self,
711        session_id: Uuid,
712        entity_name: &str,
713        relation_type: &RelationType,
714    ) -> Result<Vec<String>> {
715        match self {
716            StorageBackend::RocksDB(storage) => {
717                storage
718                    .find_related_by_type(session_id, entity_name, relation_type)
719                    .await
720            }
721            #[cfg(feature = "surrealdb-storage")]
722            StorageBackend::SurrealDB(storage) => {
723                storage
724                    .find_related_by_type(session_id, entity_name, relation_type)
725                    .await
726            }
727        }
728    }
729
730    async fn find_shortest_path(
731        &self,
732        session_id: Uuid,
733        from: &str,
734        to: &str,
735    ) -> Result<Option<Vec<String>>> {
736        match self {
737            StorageBackend::RocksDB(storage) => {
738                storage.find_shortest_path(session_id, from, to).await
739            }
740            #[cfg(feature = "surrealdb-storage")]
741            StorageBackend::SurrealDB(storage) => {
742                storage.find_shortest_path(session_id, from, to).await
743            }
744        }
745    }
746
747    async fn get_entity_network(
748        &self,
749        session_id: Uuid,
750        center: &str,
751        max_depth: usize,
752    ) -> Result<EntityNetwork> {
753        match self {
754            StorageBackend::RocksDB(storage) => {
755                storage
756                    .get_entity_network(session_id, center, max_depth)
757                    .await
758            }
759            #[cfg(feature = "surrealdb-storage")]
760            StorageBackend::SurrealDB(storage) => {
761                storage
762                    .get_entity_network(session_id, center, max_depth)
763                    .await
764            }
765        }
766    }
767}
768
769// Implement FreshnessStorage trait for StorageBackend via delegation
770#[async_trait]
771impl FreshnessStorage for StorageBackend {
772    async fn register_source(&self, session_id: Uuid, reference: SourceReference) -> Result<()> {
773        match self {
774            StorageBackend::RocksDB(storage) => {
775                storage.register_source(session_id, reference).await
776            }
777            #[cfg(feature = "surrealdb-storage")]
778            StorageBackend::SurrealDB(storage) => {
779                storage.register_source(session_id, reference).await
780            }
781        }
782    }
783
784    async fn check_freshness(&self, entry_id: &str, file_hash: &[u8]) -> Result<FreshnessEntry> {
785        match self {
786            StorageBackend::RocksDB(storage) => storage.check_freshness(entry_id, file_hash).await,
787            #[cfg(feature = "surrealdb-storage")]
788            StorageBackend::SurrealDB(storage) => {
789                storage.check_freshness(entry_id, file_hash).await
790            }
791        }
792    }
793
794    async fn invalidate_source(&self, file_path: &str) -> Result<u32> {
795        match self {
796            StorageBackend::RocksDB(storage) => storage.invalidate_source(file_path).await,
797            #[cfg(feature = "surrealdb-storage")]
798            StorageBackend::SurrealDB(storage) => storage.invalidate_source(file_path).await,
799        }
800    }
801
802    async fn get_entries_by_source(&self, file_path: &str) -> Result<Vec<SourceReference>> {
803        match self {
804            StorageBackend::RocksDB(storage) => storage.get_entries_by_source(file_path).await,
805            #[cfg(feature = "surrealdb-storage")]
806            StorageBackend::SurrealDB(storage) => storage.get_entries_by_source(file_path).await,
807        }
808    }
809
810    async fn get_stale_entries_by_source(
811        &self,
812        file_path: &str,
813    ) -> Result<Vec<StaleEntryInfo>> {
814        match self {
815            StorageBackend::RocksDB(storage) => {
816                storage.get_stale_entries_by_source(file_path).await
817            }
818            #[cfg(feature = "surrealdb-storage")]
819            StorageBackend::SurrealDB(storage) => {
820                storage.get_stale_entries_by_source(file_path).await
821            }
822        }
823    }
824
825    async fn check_freshness_semantic(
826        &self,
827        entry_id: &str,
828        file_hash: &[u8],
829        ast_hash: Option<&[u8]>,
830        symbol_name: Option<&str>,
831    ) -> Result<FreshnessEntry> {
832        match self {
833            StorageBackend::RocksDB(storage) => {
834                storage
835                    .check_freshness_semantic(entry_id, file_hash, ast_hash, symbol_name)
836                    .await
837            }
838            #[cfg(feature = "surrealdb-storage")]
839            StorageBackend::SurrealDB(storage) => {
840                storage
841                    .check_freshness_semantic(entry_id, file_hash, ast_hash, symbol_name)
842                    .await
843            }
844        }
845    }
846
847    async fn register_symbol_dependencies(
848        &self,
849        from: SymbolId,
850        to_symbols: Vec<SymbolId>,
851    ) -> Result<u32> {
852        match self {
853            StorageBackend::RocksDB(storage) => {
854                storage.register_symbol_dependencies(from, to_symbols).await
855            }
856            #[cfg(feature = "surrealdb-storage")]
857            StorageBackend::SurrealDB(storage) => {
858                storage.register_symbol_dependencies(from, to_symbols).await
859            }
860        }
861    }
862
863    async fn cascade_invalidate(
864        &self,
865        changed: SymbolId,
866        new_ast_hash: Vec<u8>,
867        max_depth: u32,
868    ) -> Result<CascadeInvalidateReport> {
869        match self {
870            StorageBackend::RocksDB(storage) => {
871                storage
872                    .cascade_invalidate(changed, new_ast_hash, max_depth)
873                    .await
874            }
875            #[cfg(feature = "surrealdb-storage")]
876            StorageBackend::SurrealDB(storage) => {
877                storage
878                    .cascade_invalidate(changed, new_ast_hash, max_depth)
879                    .await
880            }
881        }
882    }
883
884    async fn check_freshness_batch(
885        &self,
886        entries: Vec<(String, Vec<u8>, Option<Vec<u8>>, Option<String>)>,
887    ) -> Result<Vec<FreshnessEntry>> {
888        match self {
889            StorageBackend::RocksDB(storage) => storage.check_freshness_batch(entries).await,
890            #[cfg(feature = "surrealdb-storage")]
891            StorageBackend::SurrealDB(storage) => storage.check_freshness_batch(entries).await,
892        }
893    }
894}
895
896/// Storage configuration for backend selection
897#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
898pub struct StorageConfig {
899    /// Storage backend type
900    pub backend: StorageBackendType,
901
902    /// Path to storage data directory
903    pub path: std::path::PathBuf,
904
905    /// SurrealDB-specific configuration
906    #[cfg(feature = "surrealdb-storage")]
907    pub surrealdb: Option<SurrealDBConfig>,
908}
909
910/// Storage backend type
911#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
912#[serde(rename_all = "lowercase")]
913#[derive(Default)]
914pub enum StorageBackendType {
915    /// RocksDB local storage (default)
916    #[default]
917    RocksDB,
918
919    /// SurrealDB storage with native graph support
920    #[cfg(feature = "surrealdb-storage")]
921    SurrealDB,
922}
923
924
925impl StorageBackendType {
926    /// Parse storage backend type from string
927    pub fn from_str(s: &str) -> Option<Self> {
928        match s.to_lowercase().as_str() {
929            "rocksdb" | "rocks" => Some(StorageBackendType::RocksDB),
930            #[cfg(feature = "surrealdb-storage")]
931            "surrealdb" | "surreal" => Some(StorageBackendType::SurrealDB),
932            _ => None,
933        }
934    }
935}
936
937/// SurrealDB-specific configuration
938#[cfg(feature = "surrealdb-storage")]
939#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
940pub struct SurrealDBConfig {
941    /// SurrealDB namespace
942    pub namespace: String,
943
944    /// SurrealDB database name
945    pub database: String,
946
947    /// TiKV endpoints (for distributed mode)
948    pub tikv_endpoints: Option<Vec<String>>,
949}
950
951#[cfg(feature = "surrealdb-storage")]
952impl Default for SurrealDBConfig {
953    fn default() -> Self {
954        Self {
955            namespace: "post_cortex".to_string(),
956            database: "main".to_string(),
957            tikv_endpoints: None,
958        }
959    }
960}
961
962impl Default for StorageConfig {
963    fn default() -> Self {
964        Self {
965            backend: StorageBackendType::default(),
966            path: dirs::data_local_dir()
967                .unwrap_or_else(|| std::path::PathBuf::from("."))
968                .join("post-cortex")
969                .join("data"),
970            #[cfg(feature = "surrealdb-storage")]
971            surrealdb: None,
972        }
973    }
974}