1use crate::cache::EntityCache;
7use serde::Serialize;
8use serde_json::Value;
9use std::collections::{HashMap, HashSet};
10use std::sync::Arc;
11use tokio::sync::RwLock;
12
13#[derive(Debug, Clone, PartialEq)]
15pub enum ViewEffect {
16 NoEffect,
18 Add { key: String },
20 Remove { key: String },
22 Update { key: String },
24 Replace { old_key: String, new_key: String },
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
30pub enum SortOrder {
31 Asc,
32 Desc,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
37pub enum CompareOp {
38 Eq,
39 Ne,
40 Gt,
41 Gte,
42 Lt,
43 Lte,
44}
45
46#[derive(Debug)]
48pub struct MaterializedView {
49 pub id: String,
51 pub source_id: String,
53 current_keys: Arc<RwLock<HashSet<String>>>,
55 pipeline: ViewPipeline,
57}
58
59#[derive(Debug, Clone, Default, Serialize)]
60pub struct ViewPipeline {
61 pub filter: Option<FilterConfig>,
63 pub sort: Option<SortConfig>,
65 pub limit: Option<usize>,
67}
68
69#[derive(Debug, Clone, Serialize)]
70pub struct FilterConfig {
71 pub field_path: Vec<String>,
72 pub op: CompareOp,
73 pub value: Value,
74}
75
76impl FilterConfig {
77 pub fn matches(&self, entity: &Value) -> bool {
80 static NULL: Value = Value::Null;
81 let mut field = entity;
82 for segment in &self.field_path {
83 match field.get(segment) {
84 Some(value) => field = value,
85 None => {
86 field = &NULL;
87 break;
88 }
89 }
90 }
91 match self.op {
92 CompareOp::Eq => *field == self.value,
93 CompareOp::Ne => *field != self.value,
94 CompareOp::Gt => compare_values(field, &self.value) == std::cmp::Ordering::Greater,
95 CompareOp::Gte => compare_values(field, &self.value) != std::cmp::Ordering::Less,
96 CompareOp::Lt => compare_values(field, &self.value) == std::cmp::Ordering::Less,
97 CompareOp::Lte => compare_values(field, &self.value) != std::cmp::Ordering::Greater,
98 }
99 }
100}
101
102#[derive(Debug, Clone, Serialize)]
103pub struct SortConfig {
104 pub field_path: Vec<String>,
105 pub order: SortOrder,
106}
107
108impl MaterializedView {
109 pub fn new(id: String, source_id: String, pipeline: ViewPipeline) -> Self {
111 Self {
112 id,
113 source_id,
114 current_keys: Arc::new(RwLock::new(HashSet::new())),
115 pipeline,
116 }
117 }
118
119 pub async fn get_keys(&self) -> HashSet<String> {
121 self.current_keys.read().await.clone()
122 }
123
124 pub async fn evaluate_initial(&self, cache: &EntityCache) -> Vec<(String, Value)> {
126 let entities = cache.get_all(&self.source_id).await;
127 self.evaluate_pipeline(entities).await
128 }
129
130 async fn evaluate_pipeline(&self, mut entities: Vec<(String, Value)>) -> Vec<(String, Value)> {
132 if let Some(ref filter) = self.pipeline.filter {
134 entities.retain(|(_, v)| self.matches_filter(v, filter));
135 }
136
137 if let Some(ref sort) = self.pipeline.sort {
139 entities.sort_by(|(_, a), (_, b)| {
140 let a_val = extract_field(a, &sort.field_path);
141 let b_val = extract_field(b, &sort.field_path);
142 let cmp = compare_values(&a_val, &b_val);
143 match sort.order {
144 SortOrder::Asc => cmp,
145 SortOrder::Desc => cmp.reverse(),
146 }
147 });
148 }
149
150 if let Some(limit) = self.pipeline.limit {
152 entities.truncate(limit);
153 }
154
155 let keys: HashSet<String> = entities.iter().map(|(k, _)| k.clone()).collect();
157 *self.current_keys.write().await = keys;
158
159 entities
160 }
161
162 fn matches_filter(&self, entity: &Value, filter: &FilterConfig) -> bool {
164 filter.matches(entity)
165 }
166
167 pub async fn compute_effect(
169 &self,
170 key: &str,
171 new_value: Option<&Value>,
172 _cache: &EntityCache,
173 ) -> ViewEffect {
174 let current_keys = self.current_keys.read().await;
175 let was_in_view = current_keys.contains(key);
176 drop(current_keys);
177
178 let matches_now = match new_value {
180 Some(v) => {
181 if let Some(ref filter) = self.pipeline.filter {
182 self.matches_filter(v, filter)
183 } else {
184 true
185 }
186 }
187 None => false, };
189
190 match (was_in_view, matches_now) {
191 (false, true) => {
192 if self.pipeline.limit == Some(1) {
193 let current_keys = self.current_keys.read().await;
194 if let Some(current_key) = current_keys.iter().next() {
195 if current_key != key {
196 return ViewEffect::Replace {
197 old_key: current_key.clone(),
198 new_key: key.to_string(),
199 };
200 }
201 }
202 }
203 ViewEffect::Add {
204 key: key.to_string(),
205 }
206 }
207 (true, false) => ViewEffect::Remove {
208 key: key.to_string(),
209 },
210 (true, true) => ViewEffect::Update {
211 key: key.to_string(),
212 },
213 (false, false) => ViewEffect::NoEffect,
214 }
215 }
216
217 pub async fn apply_effect(&self, effect: &ViewEffect) {
219 let mut keys = self.current_keys.write().await;
220 match effect {
221 ViewEffect::Add { key } => {
222 keys.insert(key.clone());
223 }
224 ViewEffect::Remove { key } => {
225 keys.remove(key);
226 }
227 ViewEffect::Replace { old_key, new_key } => {
228 keys.remove(old_key);
229 keys.insert(new_key.clone());
230 }
231 ViewEffect::Update { .. } | ViewEffect::NoEffect => {}
232 }
233 }
234}
235
236fn extract_field(value: &Value, path: &[String]) -> Value {
238 let mut current = value;
239 for segment in path {
240 match current.get(segment) {
241 Some(v) => current = v,
242 None => return Value::Null,
243 }
244 }
245 current.clone()
246}
247
248fn compare_values(a: &Value, b: &Value) -> std::cmp::Ordering {
250 match (a, b) {
251 (Value::Number(a), Value::Number(b)) => {
252 let a_f = a.as_f64().unwrap_or(0.0);
253 let b_f = b.as_f64().unwrap_or(0.0);
254 a_f.partial_cmp(&b_f).unwrap_or(std::cmp::Ordering::Equal)
255 }
256 (Value::String(a), Value::String(b)) => a.cmp(b),
257 (Value::Bool(a), Value::Bool(b)) => a.cmp(b),
258 _ => std::cmp::Ordering::Equal,
259 }
260}
261
262#[derive(Default)]
264pub struct MaterializedViewRegistry {
265 views: HashMap<String, Arc<MaterializedView>>,
266 dependencies: HashMap<String, Vec<String>>,
268}
269
270impl MaterializedViewRegistry {
271 pub fn new() -> Self {
272 Self::default()
273 }
274
275 pub fn register(&mut self, view: MaterializedView) {
277 let view_id = view.id.clone();
278 let source_id = view.source_id.clone();
279
280 self.dependencies
281 .entry(source_id)
282 .or_default()
283 .push(view_id.clone());
284
285 self.views.insert(view_id, Arc::new(view));
286 }
287
288 pub fn get(&self, id: &str) -> Option<Arc<MaterializedView>> {
290 self.views.get(id).cloned()
291 }
292
293 pub fn get_dependents(&self, source_id: &str) -> Vec<Arc<MaterializedView>> {
295 self.dependencies
296 .get(source_id)
297 .map(|ids| {
298 ids.iter()
299 .filter_map(|id| self.views.get(id).cloned())
300 .collect()
301 })
302 .unwrap_or_default()
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309 use serde_json::json;
310
311 #[tokio::test]
312 async fn test_filter_evaluation() {
313 let pipeline = ViewPipeline {
314 filter: Some(FilterConfig {
315 field_path: vec!["status".to_string()],
316 op: CompareOp::Eq,
317 value: json!("active"),
318 }),
319 sort: None,
320 limit: None,
321 };
322
323 let view =
324 MaterializedView::new("test/active".to_string(), "test/list".to_string(), pipeline);
325
326 let entities = vec![
327 ("1".to_string(), json!({"status": "active", "value": 10})),
328 ("2".to_string(), json!({"status": "inactive", "value": 20})),
329 ("3".to_string(), json!({"status": "active", "value": 30})),
330 ];
331
332 let result = view.evaluate_pipeline(entities).await;
333 assert_eq!(result.len(), 2);
334 assert_eq!(result[0].0, "1");
335 assert_eq!(result[1].0, "3");
336 }
337
338 #[tokio::test]
339 async fn test_sort_and_limit() {
340 let pipeline = ViewPipeline {
341 filter: None,
342 sort: Some(SortConfig {
343 field_path: vec!["value".to_string()],
344 order: SortOrder::Desc,
345 }),
346 limit: Some(2),
347 };
348
349 let view =
350 MaterializedView::new("test/top2".to_string(), "test/list".to_string(), pipeline);
351
352 let entities = vec![
353 ("1".to_string(), json!({"value": 10})),
354 ("2".to_string(), json!({"value": 30})),
355 ("3".to_string(), json!({"value": 20})),
356 ];
357
358 let result = view.evaluate_pipeline(entities).await;
359 assert_eq!(result.len(), 2);
360 assert_eq!(result[0].0, "2"); assert_eq!(result[1].0, "3"); }
363}