Skip to main content

kotoba_projection_engine/
lib.rs

1//! `kotoba-projection-engine`
2//!
3//! Real-time projection engine for GraphDB materialization using RocksDB.
4//! Processes events from event streams and maintains materialized views in GraphDB.
5
6use std::collections::HashMap;
7use std::sync::Arc;
8use tokio::sync::{RwLock, mpsc};
9use dashmap::DashMap;
10use anyhow::{Result, Context};
11use tracing::{info, warn, error, instrument};
12// use metrics::macros::counter; // Temporarily disabled due to version issues
13use serde::{Deserialize, Serialize};
14use chrono::{DateTime, Utc};
15
16use kotoba_ocel::OcelEvent;
17use kotoba_storage::KeyValueStore;
18
19pub mod event_processor;
20pub mod materializer;
21pub mod view_manager;
22pub mod storage;
23pub mod cache_integration;
24pub mod metrics;
25pub mod gql_integration;
26
27// Re-export main types
28pub use event_processor::*;
29pub use materializer::*;
30pub use view_manager::*;
31pub use storage::*;
32pub use cache_integration::*;
33pub use metrics::*;
34
35/// Main projection engine with generic KeyValueStore backend
36pub struct ProjectionEngine<T: KeyValueStore> {
37    /// Event processor for handling OCEL events
38    event_processor: Arc<EventProcessor<T>>,
39    /// Materializer for projections
40    materializer: Arc<Materializer<T>>,
41    /// Storage backend
42    storage: Arc<T>,
43    /// View manager
44    view_manager: Arc<ViewManager>,
45    /// Metrics collector
46    metrics: Arc<MetricsCollector>,
47    /// Engine configuration
48    config: ProjectionConfig,
49    /// Active projections
50    active_projections: Arc<DashMap<String, ProjectionState>>,
51    /// Shutdown signal
52    shutdown_tx: mpsc::Sender<()>,
53    shutdown_rx: Arc<RwLock<mpsc::Receiver<()>>>,
54}
55
56/// Projection configuration
57#[derive(Debug, Clone)]
58pub struct ProjectionConfig {
59    /// Storage prefix for projection keys
60    pub storage_prefix: String,
61    /// Maximum concurrent projections
62    pub max_concurrent_projections: usize,
63    /// Batch size for event processing
64    pub batch_size: usize,
65    /// Checkpoint interval (in events)
66    pub checkpoint_interval: u64,
67    /// Metrics collection
68    pub enable_metrics: bool,
69}
70
71impl Default for ProjectionConfig {
72    fn default() -> Self {
73        Self {
74            storage_prefix: "projections".to_string(),
75            max_concurrent_projections: 10,
76            batch_size: 100,
77            checkpoint_interval: 1000,
78            enable_metrics: true,
79        }
80    }
81}
82
83
84
85
86/// Projection state
87#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
88pub struct ProjectionState {
89    /// Projection name
90    pub name: String,
91    /// Current event sequence number
92    pub sequence_number: u64,
93    /// Last checkpoint timestamp
94    pub last_checkpoint: chrono::DateTime<chrono::Utc>,
95    /// Projection status
96    pub status: ProjectionStatus,
97    /// Processing statistics
98    pub stats: ProjectionStats,
99}
100
101/// Projection status
102#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
103pub enum ProjectionStatus {
104    /// Projection is active and processing events
105    Active,
106    /// Projection is paused
107    Paused,
108    /// Projection encountered an error
109    Error(String),
110    /// Projection is being rebuilt
111    Rebuilding,
112}
113
114/// Projection statistics
115#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
116pub struct ProjectionStats {
117    /// Total events processed
118    pub events_processed: u64,
119    /// Events processed per second
120    pub events_per_second: f64,
121    /// Total processing time
122    pub total_processing_time_ms: u64,
123    /// Average processing time per event
124    pub avg_processing_time_ms: f64,
125    /// Number of cache hits
126    pub cache_hits: u64,
127    /// Number of cache misses
128    pub cache_misses: u64,
129}
130
131impl<T: KeyValueStore + 'static> ProjectionEngine<T> {
132    /// Create a new projection engine with the given KeyValueStore backend
133    pub fn new(config: ProjectionConfig, storage: Arc<T>) -> Self {
134        info!("Initializing Projection Engine with config: {:?}", config);
135
136        // Initialize view manager
137        let view_manager = Arc::new(ViewManager::new());
138
139        // Initialize metrics
140        let metrics = Arc::new(MetricsCollector::new());
141
142        // Initialize materializer with storage backend
143        let materializer = Arc::new(Materializer::new(
144            storage.clone(),
145            config.storage_prefix.clone(),
146        ));
147
148        // Initialize event processor
149        let event_processor = Arc::new(EventProcessor::new(
150            materializer.clone(),
151            config.batch_size,
152        ));
153
154        // Create shutdown channel
155        let (shutdown_tx, shutdown_rx) = mpsc::channel(1);
156
157        let engine = Self {
158            event_processor,
159            materializer,
160            storage,
161            view_manager,
162            metrics,
163            config,
164            active_projections: Arc::new(DashMap::new()),
165            shutdown_tx,
166            shutdown_rx: Arc::new(RwLock::new(shutdown_rx)),
167        };
168
169        info!("Projection Engine initialized successfully");
170        engine
171    }
172}
173
174impl<T: KeyValueStore> Clone for ProjectionEngine<T> {
175    fn clone(&self) -> Self {
176        Self {
177            event_processor: self.event_processor.clone(),
178            materializer: self.materializer.clone(),
179            storage: self.storage.clone(),
180            view_manager: self.view_manager.clone(),
181            metrics: self.metrics.clone(),
182            config: self.config.clone(),
183            active_projections: self.active_projections.clone(),
184            shutdown_tx: self.shutdown_tx.clone(),
185            shutdown_rx: self.shutdown_rx.clone(),
186        }
187    }
188}
189
190impl<T: KeyValueStore + 'static> ProjectionEngine<T> {
191    /// Start the projection engine
192    #[instrument(skip(self))]
193    pub async fn start(&self) -> Result<()> {
194        info!("Starting Projection Engine");
195
196        // Start event processor
197        self.event_processor.start().await?;
198
199        // Load existing projections
200        self.load_existing_projections().await?;
201
202        // Start metrics collection if enabled
203        if self.config.enable_metrics {
204            self.start_metrics_collection().await?;
205        }
206
207        info!("Projection Engine started successfully");
208        Ok(())
209    }
210
211    /// Stop the projection engine
212    #[instrument(skip(self))]
213    pub async fn stop(&self) -> Result<()> {
214        info!("Stopping Projection Engine");
215
216        // Signal shutdown
217        let _ = self.shutdown_tx.send(()).await;
218
219        // Stop event processor
220        self.event_processor.stop().await?;
221
222        // Save projection states
223        self.save_projection_states().await?;
224
225        info!("Projection Engine stopped successfully");
226        Ok(())
227    }
228
229    /// Create a new projection
230    #[instrument(skip(self))]
231    pub async fn create_projection(
232        &self,
233        name: String,
234        definition: ProjectionDefinition,
235    ) -> Result<()> {
236        info!("Creating projection: {}", name);
237
238        // Validate projection definition
239        self.validate_projection_definition(&definition).await?;
240
241        // Create projection in view manager
242        self.view_manager.create_projection(name.clone(), definition).await?;
243
244        // Initialize projection state
245        let state = ProjectionState {
246            name: name.clone(),
247            sequence_number: 0,
248            last_checkpoint: chrono::Utc::now(),
249            status: ProjectionStatus::Active,
250            stats: ProjectionStats::default(),
251        };
252
253        self.active_projections.insert(name.clone(), state);
254
255        // Register with event processor
256        self.event_processor.register_projection(&name).await?;
257
258        info!("Projection created successfully: {}", name);
259        Ok(())
260    }
261
262    /// Delete a projection
263    #[instrument(skip(self))]
264    pub async fn delete_projection(&self, name: &str) -> Result<()> {
265        info!("Deleting projection: {}", name);
266
267        // Unregister from event processor
268        self.event_processor.unregister_projection(name).await?;
269
270        // Delete from view manager
271        self.view_manager.delete_projection(name).await?;
272
273        // Remove from active projections
274        self.active_projections.remove(name);
275
276        info!("Projection deleted successfully: {}", name);
277        Ok(())
278    }
279
280    /// Get projection status
281    pub async fn get_projection_status(&self, name: &str) -> Result<Option<ProjectionState>> {
282        Ok(self.active_projections.get(name).map(|s| s.clone()))
283    }
284
285    /// List all projections
286    pub async fn list_projections(&self) -> Vec<String> {
287        self.active_projections.iter().map(|p| p.key().clone()).collect()
288    }
289
290    /// Process a batch of OCEL events
291    #[instrument(skip(self, events))]
292    pub async fn process_ocel_events(&self, events: Vec<OcelEvent>) -> Result<()> {
293        if self.config.enable_metrics {
294            // counter!("projection_engine.events_received", events.len() as u64);
295        }
296
297        // Process OCEL events through the event processor
298        self.event_processor.process_batch(events).await
299    }
300
301    /// Process a batch of events (legacy method)
302    #[instrument(skip(self, events))]
303    pub async fn process_events(&self, events: Vec<EventEnvelope>) -> Result<()> {
304        warn!("Legacy event processing is deprecated. Use process_ocel_events instead.");
305        Ok(())
306    }
307
308    /// Query projections using storage backend
309    #[instrument(skip(self, query))]
310    pub async fn query_projections(&self, query: serde_json::Value) -> Result<serde_json::Value> {
311        // For now, implement basic key scanning
312        // This would be enhanced with proper GraphQL/GQL query support
313        warn!("query_projections is not fully implemented yet");
314
315        // Return empty result for now
316        Ok(serde_json::json!({
317            "columns": [],
318            "rows": [],
319            "statistics": {
320                "total_rows": 0,
321                "execution_time_ms": 0
322            }
323        }))
324    }
325
326    /// Query a materialized view (legacy method)
327    #[instrument(skip(self, query))]
328    pub async fn query_view(&self, projection_name: &str, query: ViewQuery) -> Result<ViewResult> {
329        warn!("Legacy view querying is deprecated. Use query_graph instead.");
330        Err(anyhow::anyhow!("Legacy view querying not supported"))
331    }
332
333    /// Get engine statistics
334    pub async fn get_statistics(&self) -> EngineStatistics {
335        let mut total_events = 0u64;
336        let mut active_projections = 0usize;
337
338        for projection in self.active_projections.iter() {
339            total_events += projection.stats.events_processed;
340            if matches!(projection.status, ProjectionStatus::Active) {
341                active_projections += 1;
342            }
343        }
344
345        // For now, return 0 for storage size as we don't have a generic way to get size
346        EngineStatistics {
347            total_projections: self.active_projections.len(),
348            active_projections,
349            total_events_processed: total_events,
350            uptime_seconds: 0, // TODO: Track uptime
351            storage_size_bytes: 0, // TODO: Implement storage size calculation
352        }
353    }
354
355    /// Execute GQL query (simplified implementation)
356    #[instrument(skip(self))]
357    pub async fn execute_gql_query(
358        &self,
359        query: &str,
360        _user_id: Option<String>,
361        _timeout_seconds: u64,
362        _parameters: std::collections::HashMap<String, serde_json::Value>,
363    ) -> Result<serde_json::Value> {
364        use crate::gql_integration::ProjectionEngineAdapter;
365
366        let adapter = ProjectionEngineAdapter::new(Arc::new(self.clone()));
367        adapter.execute_gql_query(query, serde_json::json!({})).await
368    }
369
370    /// Execute GQL statement (DDL/DML) (simplified implementation)
371    #[instrument(skip(self))]
372    pub async fn execute_gql_statement(
373        &self,
374        statement: &str,
375        _user_id: Option<String>,
376        _timeout_seconds: u64,
377        _parameters: std::collections::HashMap<String, serde_json::Value>,
378    ) -> Result<serde_json::Value> {
379        use crate::gql_integration::ProjectionEngineAdapter;
380
381        let adapter = ProjectionEngineAdapter::new(Arc::new(self.clone()));
382        adapter.execute_gql_statement(statement, serde_json::json!({})).await
383    }
384
385    async fn load_existing_projections(&self) -> Result<()> {
386        info!("Loading existing projections");
387
388        // Scan for projection keys in storage
389        let prefix = format!("{}:projection:", self.config.storage_prefix);
390        let projection_keys = self.storage.scan(prefix.as_bytes()).await?;
391
392        for key_bytes in projection_keys {
393            if let Ok(key_str) = std::str::from_utf8(&key_bytes.0) {
394                if let Some(projection_name) = key_str.strip_prefix(&prefix) {
395                    // Load projection state from storage
396                    let state_key = format!("{}:state:{}", self.config.storage_prefix, projection_name);
397                    if let Some(state_data) = self.storage.get(state_key.as_bytes()).await? {
398                        if let Ok(state) = bincode::deserialize::<ProjectionState>(&state_data) {
399                            self.active_projections.insert(projection_name.to_string(), state);
400                            self.event_processor.register_projection(projection_name).await?;
401                        }
402                    }
403                }
404            }
405        }
406
407        info!("Loaded {} existing projections", self.active_projections.len());
408        Ok(())
409    }
410
411    async fn save_projection_states(&self) -> Result<()> {
412        for projection in self.active_projections.iter() {
413            let state_key = format!("{}:state:{}", self.config.storage_prefix, projection.key());
414            let state_data = bincode::serialize(&projection.value())?;
415            self.storage.put(state_key.as_bytes(), &state_data).await?;
416        }
417        Ok(())
418    }
419
420    async fn validate_projection_definition(&self, definition: &ProjectionDefinition) -> Result<()> {
421        // TODO: Implement validation logic
422        Ok(())
423    }
424
425    async fn start_metrics_collection(&self) -> Result<()> {
426        // TODO: Start metrics collection task
427        Ok(())
428    }
429}
430
431/// Engine statistics
432#[derive(Debug, Clone, Serialize, Deserialize)]
433pub struct EngineStatistics {
434    pub total_projections: usize,
435    pub active_projections: usize,
436    pub total_events_processed: u64,
437    pub uptime_seconds: u64,
438    pub storage_size_bytes: u64,
439}
440
441// Placeholder types - will be defined in respective modules
442pub type EventEnvelope = serde_json::Value;
443pub type ProjectionDefinition = serde_json::Value;
444// Placeholder types - these will be replaced with actual implementations
445pub type QueryResult = serde_json::Value; // Use JSON for now to handle type conversion
446pub type ViewQuery = serde_json::Value;
447pub type ViewResult = serde_json::Value;
448
449impl Default for ProjectionStats {
450    fn default() -> Self {
451        Self {
452            events_processed: 0,
453            events_per_second: 0.0,
454            total_processing_time_ms: 0,
455            avg_processing_time_ms: 0.0,
456            cache_hits: 0,
457            cache_misses: 0,
458        }
459    }
460}
461
462#[cfg(test)]
463mod tests {
464    use super::*;
465    use tempfile::tempdir;
466
467    #[tokio::test]
468    async fn test_projection_engine_creation() {
469        let temp_dir = tempdir().unwrap();
470        let config = ProjectionConfig::default();
471
472        // Create a temporary storage backend (for testing, we could use memory-based)
473        // For now, we'll skip the actual creation test as it requires a concrete KeyValueStore
474        let stats_placeholder = EngineStatistics {
475            total_projections: 0,
476            active_projections: 0,
477            total_events_processed: 0,
478            uptime_seconds: 0,
479            storage_size_bytes: 0,
480        };
481        assert_eq!(stats_placeholder.total_projections, 0);
482    }
483
484    #[tokio::test]
485    async fn test_projection_lifecycle() {
486        let config = ProjectionConfig::default();
487
488        // Skip actual engine creation test as it requires concrete KeyValueStore implementation
489        // This would need to be tested with a real storage backend like RocksDB adapter
490        let projection_def = serde_json::json!({
491            "name": "test_projection",
492            "source_events": ["node.created", "edge.created"],
493            "target_view": "test_view"
494        });
495
496        assert_eq!(projection_def["name"], "test_projection");
497
498    }
499
500    #[tokio::test]
501    async fn test_gql_integration() {
502        let config = ProjectionConfig::default();
503
504        // Skip actual GQL integration test as it requires concrete KeyValueStore implementation
505        // This would need to be tested with a real storage backend like RocksDB adapter
506
507        let query = "MATCH (v:Person) RETURN v";
508        // Test that GQL query structure is valid
509        assert!(!query.is_empty(), "GQL query should not be empty");
510
511        let statement = "CREATE GRAPH test_graph";
512        assert!(!statement.is_empty(), "GQL statement should not be empty");
513    }
514}