aurora_db/
computed.rs

1// Computed Fields - Auto-calculated field values
2//
3// Supports dynamic field calculation based on other field values
4// Examples: full_name from first_name + last_name, age from birthdate, etc.
5
6use crate::error::Result;
7use crate::types::{Document, Value};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11/// Computation expression for a field
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub enum ComputedExpression {
14    /// Concatenate string fields
15    Concat(Vec<String>),
16    /// Sum numeric fields
17    Sum(Vec<String>),
18    /// Multiply numeric fields
19    Product(Vec<String>),
20    /// Average numeric fields
21    Average(Vec<String>),
22    /// JavaScript-like custom expression (field name -> value extraction)
23    Custom(String),
24}
25
26impl ComputedExpression {
27    /// Evaluate the expression against a document
28    pub fn evaluate(&self, doc: &Document) -> Option<Value> {
29        match self {
30            ComputedExpression::Concat(fields) => {
31                let mut result = String::new();
32                for field in fields {
33                    if let Some(value) = doc.data.get(field)
34                        && let Some(s) = value.as_str() {
35                            if !result.is_empty() {
36                                result.push(' ');
37                            }
38                            result.push_str(s);
39                        }
40                }
41                Some(Value::String(result))
42            }
43
44            ComputedExpression::Sum(fields) => {
45                let mut sum = 0i64;
46                for field in fields {
47                    if let Some(value) = doc.data.get(field)
48                        && let Some(i) = value.as_i64() {
49                            sum += i;
50                        }
51                }
52                Some(Value::Int(sum))
53            }
54
55            ComputedExpression::Product(fields) => {
56                let mut product = 1i64;
57                for field in fields {
58                    if let Some(value) = doc.data.get(field)
59                        && let Some(i) = value.as_i64() {
60                            product *= i;
61                        }
62                }
63                Some(Value::Int(product))
64            }
65
66            ComputedExpression::Average(fields) => {
67                let mut sum = 0.0;
68                let mut count = 0;
69                for field in fields {
70                    if let Some(value) = doc.data.get(field) {
71                        if let Some(f) = value.as_f64() {
72                            sum += f;
73                            count += 1;
74                        } else if let Some(i) = value.as_i64() {
75                            sum += i as f64;
76                            count += 1;
77                        }
78                    }
79                }
80                if count > 0 {
81                    Some(Value::Float(sum / count as f64))
82                } else {
83                    None
84                }
85            }
86
87            ComputedExpression::Custom(_expr) => {
88                // For now, custom expressions are not implemented
89                // Could use a JS runtime like deno_core or rquickjs
90                None
91            }
92        }
93    }
94}
95
96/// Computed field registry
97pub struct ComputedFields {
98    // collection_name -> (field_name -> expression)
99    fields: HashMap<String, HashMap<String, ComputedExpression>>,
100}
101
102impl ComputedFields {
103    pub fn new() -> Self {
104        Self {
105            fields: HashMap::new(),
106        }
107    }
108
109    /// Register a computed field
110    pub fn register(
111        &mut self,
112        collection: impl Into<String>,
113        field: impl Into<String>,
114        expression: ComputedExpression,
115    ) {
116        let collection = collection.into();
117        self.fields
118            .entry(collection)
119            .or_default()
120            .insert(field.into(), expression);
121    }
122
123    /// Apply computed fields to a document
124    pub fn apply(&self, collection: &str, doc: &mut Document) -> Result<()> {
125        if let Some(computed) = self.fields.get(collection) {
126            for (field_name, expression) in computed {
127                if let Some(value) = expression.evaluate(doc) {
128                    doc.data.insert(field_name.clone(), value);
129                }
130            }
131        }
132        Ok(())
133    }
134
135    /// Get computed fields for a collection
136    pub fn get_fields(&self, collection: &str) -> Option<&HashMap<String, ComputedExpression>> {
137        self.fields.get(collection)
138    }
139}
140
141impl Default for ComputedFields {
142    fn default() -> Self {
143        Self::new()
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn test_concat_expression() {
153        let expr =
154            ComputedExpression::Concat(vec!["first_name".to_string(), "last_name".to_string()]);
155
156        let mut doc = Document::new();
157        doc.data
158            .insert("first_name".to_string(), Value::String("John".to_string()));
159        doc.data
160            .insert("last_name".to_string(), Value::String("Doe".to_string()));
161
162        let result = expr.evaluate(&doc);
163        assert_eq!(result, Some(Value::String("John Doe".to_string())));
164    }
165
166    #[test]
167    fn test_sum_expression() {
168        let expr = ComputedExpression::Sum(vec!["a".to_string(), "b".to_string(), "c".to_string()]);
169
170        let mut doc = Document::new();
171        doc.data.insert("a".to_string(), Value::Int(10));
172        doc.data.insert("b".to_string(), Value::Int(20));
173        doc.data.insert("c".to_string(), Value::Int(30));
174
175        let result = expr.evaluate(&doc);
176        assert_eq!(result, Some(Value::Int(60)));
177    }
178
179    #[test]
180    fn test_average_expression() {
181        let expr = ComputedExpression::Average(vec!["score1".to_string(), "score2".to_string()]);
182
183        let mut doc = Document::new();
184        doc.data.insert("score1".to_string(), Value::Float(85.5));
185        doc.data.insert("score2".to_string(), Value::Float(92.5));
186
187        let result = expr.evaluate(&doc);
188        assert_eq!(result, Some(Value::Float(89.0)));
189    }
190
191    #[test]
192    fn test_computed_fields_registry() {
193        let mut registry = ComputedFields::new();
194
195        registry.register(
196            "users",
197            "full_name",
198            ComputedExpression::Concat(vec!["first_name".to_string(), "last_name".to_string()]),
199        );
200
201        let mut doc = Document::new();
202        doc.data
203            .insert("first_name".to_string(), Value::String("Jane".to_string()));
204        doc.data
205            .insert("last_name".to_string(), Value::String("Smith".to_string()));
206
207        registry.apply("users", &mut doc).unwrap();
208
209        assert_eq!(
210            doc.data.get("full_name"),
211            Some(&Value::String("Jane Smith".to_string()))
212        );
213    }
214}