dsq_core/ops/
assignment_ops.rs

1use crate::error::{Error, Result};
2use crate::Value;
3
4use super::Operation;
5
6pub struct AssignAddOperation {
7    pub target_ops: Vec<Box<dyn Operation + Send + Sync>>,
8    pub value_ops: Vec<Box<dyn Operation + Send + Sync>>,
9}
10
11impl AssignAddOperation {
12    #[must_use]
13    pub fn new(
14        target_ops: Vec<Box<dyn Operation + Send + Sync>>,
15        value_ops: Vec<Box<dyn Operation + Send + Sync>>,
16    ) -> Self {
17        Self {
18            target_ops,
19            value_ops,
20        }
21    }
22
23    fn get_target_field(&self) -> Option<String> {
24        // Check if the last operation is FieldAccessOperation
25        self.target_ops.last().and_then(|last_op| {
26            // This is a bit hacky, but we can downcast to FieldAccessOperation
27            // For now, assume it's FieldAccessOperation and get the field
28            // Since we can't downcast easily, let's check the description
29            let desc = last_op.description();
30            desc.strip_prefix("field access: ")
31                .map(std::string::ToString::to_string)
32        })
33    }
34
35    #[allow(clippy::unused_self)]
36    fn apply_to_dataframe(
37        &self,
38        df: &polars::prelude::DataFrame,
39        field_name: &str,
40        add_val: &Value,
41    ) -> Result<Value> {
42        // Get the current column
43        let current_column = df
44            .column(field_name)
45            .map_err(|_| Error::operation(format!("Field '{field_name}' not found")))?;
46
47        // Add the value to the column
48        let new_column = match add_val {
49            Value::Int(i) => current_column + *i,
50            Value::Float(f) => current_column + *f,
51            _ => return Err(Error::operation("Can only add numeric values to columns")),
52        };
53
54        // Convert column to series for replacement
55        let new_series = new_column.as_materialized_series().clone();
56
57        // Create new DataFrame with the modified column
58        let mut new_df = df.clone();
59        new_df
60            .replace(field_name, new_series)
61            .map_err(|e| Error::operation(format!("Failed to replace column: {e}")))?;
62
63        Ok(Value::DataFrame(new_df))
64    }
65}
66
67impl Operation for AssignAddOperation {
68    fn apply(&self, value: &Value) -> Result<Value> {
69        if let Value::DataFrame(df) = value {
70            // For DataFrame, find the field name from target_ops
71            if let Some(field_name) = self.get_target_field() {
72                // Get the value to add
73                let mut add_val = value.clone();
74                for op in &self.value_ops {
75                    add_val = op.apply(&add_val)?;
76                }
77
78                // Modify the DataFrame
79                self.apply_to_dataframe(df, &field_name, &add_val)
80            } else {
81                Err(Error::operation("Assignment target must be a field access"))
82            }
83        } else {
84            // For other values, apply as before (though this may not work well)
85            let mut target_val = value.clone();
86            for op in &self.target_ops {
87                target_val = op.apply(&target_val)?;
88            }
89
90            let mut add_val = value.clone();
91            for op in &self.value_ops {
92                add_val = op.apply(&add_val)?;
93            }
94
95            Ok(dsq_shared::ops::add_values(&target_val, &add_val)?)
96        }
97    }
98
99    fn description(&self) -> String {
100        "assign add".to_string()
101    }
102}
103
104pub struct AssignUpdateOperation {
105    pub target_ops: Vec<Box<dyn Operation + Send + Sync>>,
106    pub value_ops: Vec<Box<dyn Operation + Send + Sync>>,
107}
108
109impl AssignUpdateOperation {
110    #[must_use]
111    pub fn new(
112        target_ops: Vec<Box<dyn Operation + Send + Sync>>,
113        value_ops: Vec<Box<dyn Operation + Send + Sync>>,
114    ) -> Self {
115        Self {
116            target_ops,
117            value_ops,
118        }
119    }
120
121    /// Helper method to update nested fields in an object
122    fn update_nested_field(
123        obj: &mut std::collections::HashMap<String, Value>,
124        fields: &[&str],
125        value: Value,
126    ) -> Result<()> {
127        if fields.is_empty() {
128            return Err(crate::error::Error::operation("Empty field path"));
129        }
130
131        if fields.len() == 1 {
132            obj.insert(fields[0].to_string(), value);
133            return Ok(());
134        }
135
136        // Navigate to the nested object
137        let mut current = obj;
138        for &field in &fields[..fields.len() - 1] {
139            match current.get_mut(field) {
140                Some(Value::Object(ref mut nested_obj)) => {
141                    current = nested_obj;
142                }
143                Some(_) => {
144                    return Err(crate::error::Error::operation(format!(
145                        "Field '{field}' is not an object"
146                    )));
147                }
148                None => {
149                    return Err(crate::error::Error::operation(format!(
150                        "Field '{field}' not found"
151                    )));
152                }
153            }
154        }
155
156        // Update the final field
157        let last_field = fields[fields.len() - 1];
158        current.insert(last_field.to_string(), value);
159        Ok(())
160    }
161}
162
163impl Operation for AssignUpdateOperation {
164    fn apply(&self, value: &Value) -> Result<Value> {
165        // For assignment on objects, we need to modify the current value
166        if let Value::Object(ref obj) = value {
167            // Check if target is a field access
168            if self.target_ops.len() == 1 {
169                if let Some(field_op) = self.target_ops.first() {
170                    // Check if it's a FieldAccessOperation by trying to downcast
171                    // For now, check description for field access
172                    let desc = field_op.description();
173                    if let Some(field_path) = desc.strip_prefix("field access: ") {
174                        // Remove "field access: "
175                        let fields: Vec<&str> = field_path.split('.').collect();
176
177                        // Evaluate the value
178                        let mut value_val = value.clone();
179                        for op in &self.value_ops {
180                            value_val = op.apply(&value_val)?;
181                        }
182
183                        // Create new object with updated field
184                        let mut new_obj = obj.clone();
185                        if fields.len() == 1 {
186                            // Simple field update
187                            new_obj.insert(field_path.to_string(), value_val);
188                        } else {
189                            // Nested field update
190                            Self::update_nested_field(&mut new_obj, &fields, value_val)?;
191                        }
192                        return Ok(Value::Object(new_obj));
193                    }
194                }
195            }
196        }
197
198        // Fallback: evaluate target and value, then return value
199        let mut value_val = value.clone();
200        for op in &self.value_ops {
201            value_val = op.apply(&value_val)?;
202        }
203
204        Ok(value_val)
205    }
206
207    fn description(&self) -> String {
208        "assign update".to_string()
209    }
210}