1use std::cmp::Ordering;
21
22use hidane_proto::google::firestore::v1::{
23 ArrayValue, Value,
24 document_transform::{
25 FieldTransform,
26 field_transform::{ServerValue, TransformType},
27 },
28 value::ValueType,
29};
30use prost_types::Timestamp;
31
32use crate::{
33 field_path::{FieldPath, Fields, get, set},
34 order::{Number, compare, compare_number},
35};
36
37pub fn apply(
40 fields: &mut Fields,
41 transform: &FieldTransform,
42 commit_time: &Timestamp,
43) -> Result<Value, String> {
44 let path = FieldPath::parse(&transform.field_path)?;
45 apply_at(fields, &path, transform, commit_time)
46}
47
48pub fn apply_at(
51 fields: &mut Fields,
52 path: &FieldPath,
53 transform: &FieldTransform,
54 commit_time: &Timestamp,
55) -> Result<Value, String> {
56 let current = get(fields, path);
57 let (stored, result) = match transform
58 .transform_type
59 .as_ref()
60 .ok_or("A field transform needs a transform type.")?
61 {
62 TransformType::SetToServerValue(kind) => {
63 if *kind != ServerValue::RequestTime as i32 {
64 return Err("Unsupported server value.".into());
65 }
66 let value = timestamp(request_time(commit_time));
67 (value.clone(), value)
68 }
69 TransformType::Increment(operand) => {
70 let operand_number = number(operand).ok_or(NOT_A_NUMBER)?;
71 let value = match current.and_then(number) {
72 Some(current) => from_number(add(current, operand_number)),
73 None => operand.clone(),
74 };
75 (value.clone(), value)
76 }
77 TransformType::Maximum(operand) => {
78 let value = extreme(current, operand, Ordering::Greater)?;
79 (value.clone(), value)
80 }
81 TransformType::Minimum(operand) => {
82 let value = extreme(current, operand, Ordering::Less)?;
83 (value.clone(), value)
84 }
85 TransformType::AppendMissingElements(operands) => {
86 let mut elements = array_elements(current);
87 for operand in &operands.values {
88 if !elements.iter().any(|e| equal(e, operand)) {
89 elements.push(operand.clone());
90 }
91 }
92 (array(elements), null())
93 }
94 TransformType::RemoveAllFromArray(operands) => {
95 let mut elements = array_elements(current);
96 elements.retain(|e| !operands.values.iter().any(|operand| equal(e, operand)));
97 (array(elements), null())
98 }
99 };
100 set(fields, path, stored);
101 Ok(result)
102}
103
104const NOT_A_NUMBER: &str = "Input must be int64 or double.";
105
106pub fn request_time(commit_time: &Timestamp) -> Timestamp {
108 Timestamp {
109 seconds: commit_time.seconds,
110 nanos: commit_time.nanos - commit_time.nanos % 1_000_000,
111 }
112}
113
114fn extreme(current: Option<&Value>, operand: &Value, keep: Ordering) -> Result<Value, String> {
115 let operand_number = number(operand).ok_or(NOT_A_NUMBER)?;
116 let Some(current_value) = current.filter(|v| number(v).is_some()) else {
117 return Ok(operand.clone());
118 };
119 let current_number = number(current_value).unwrap_or(Number::Integer(0));
120 if is_nan(current_number) {
121 return Ok(current_value.clone());
122 }
123 if is_nan(operand_number) {
124 return Ok(operand.clone());
125 }
126 Ok(if compare_number(operand_number, current_number) == keep {
128 operand.clone()
129 } else {
130 current_value.clone()
131 })
132}
133
134fn is_nan(n: Number) -> bool {
135 matches!(n, Number::Double(d) if d.is_nan())
136}
137
138fn add(a: Number, b: Number) -> Number {
139 match (a, b) {
140 (Number::Integer(x), Number::Integer(y)) => Number::Integer(x.saturating_add(y)),
141 (x, y) => Number::Double(as_f64(x) + as_f64(y)),
142 }
143}
144
145#[allow(clippy::cast_precision_loss)]
146fn as_f64(n: Number) -> f64 {
147 match n {
148 Number::Integer(i) => i as f64,
149 Number::Double(d) => d,
150 }
151}
152
153fn number(value: &Value) -> Option<Number> {
154 value.value_type.as_ref().and_then(Number::of)
155}
156
157fn from_number(n: Number) -> Value {
158 Value {
159 value_type: Some(match n {
160 Number::Integer(i) => ValueType::IntegerValue(i),
161 Number::Double(d) => ValueType::DoubleValue(d),
162 }),
163 }
164}
165
166fn equal(a: &Value, b: &Value) -> bool {
167 compare(a, b) == Ordering::Equal
168}
169
170fn array_elements(current: Option<&Value>) -> Vec<Value> {
171 match current.and_then(|v| v.value_type.as_ref()) {
172 Some(ValueType::ArrayValue(array)) => array.values.clone(),
173 _ => Vec::new(),
174 }
175}
176
177fn array(values: Vec<Value>) -> Value {
178 Value {
179 value_type: Some(ValueType::ArrayValue(ArrayValue { values })),
180 }
181}
182
183fn timestamp(ts: Timestamp) -> Value {
184 Value {
185 value_type: Some(ValueType::TimestampValue(ts)),
186 }
187}
188
189fn null() -> Value {
190 Value {
191 value_type: Some(ValueType::NullValue(0)),
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use hidane_proto::google::firestore::v1::MapValue;
198
199 use super::*;
200
201 fn int(i: i64) -> Value {
202 Value {
203 value_type: Some(ValueType::IntegerValue(i)),
204 }
205 }
206
207 fn double(d: f64) -> Value {
208 Value {
209 value_type: Some(ValueType::DoubleValue(d)),
210 }
211 }
212
213 fn run(fields: &mut Fields, path: &str, kind: TransformType) -> Value {
214 let transform = FieldTransform {
215 field_path: path.to_owned(),
216 transform_type: Some(kind),
217 };
218 apply(
219 fields,
220 &transform,
221 &Timestamp {
222 seconds: 10,
223 nanos: 123_456_789,
224 },
225 )
226 .unwrap()
227 }
228
229 fn field(fields: &Fields, name: &str) -> Value {
230 fields[name].clone()
231 }
232
233 #[test]
234 fn server_time_has_millisecond_precision() {
235 let mut fields = Fields::new();
236 let result = run(&mut fields, "t", TransformType::SetToServerValue(1));
237 assert_eq!(
238 result,
239 timestamp(Timestamp {
240 seconds: 10,
241 nanos: 123_000_000
242 })
243 );
244 assert_eq!(field(&fields, "t"), result);
245 }
246
247 #[test]
248 fn increments_saturate_in_both_directions() {
249 let mut fields: Fields = [
250 ("hi".into(), int(i64::MAX - 1)),
251 ("lo".into(), int(i64::MIN + 1)),
252 ]
253 .into();
254 assert_eq!(
255 run(&mut fields, "hi", TransformType::Increment(int(5))),
256 int(i64::MAX)
257 );
258 assert_eq!(
259 run(&mut fields, "lo", TransformType::Increment(int(-5))),
260 int(i64::MIN)
261 );
262 }
263
264 #[test]
265 fn extremes_keep_the_stored_value_when_equal_and_propagate_nan() {
266 let mut fields: Fields = [
267 ("neg_zero".into(), double(-0.0)),
268 ("stored_nan".into(), double(f64::NAN)),
269 ("three".into(), int(3)),
270 ]
271 .into();
272 let kept = run(&mut fields, "neg_zero", TransformType::Maximum(double(0.0)));
273 let Some(ValueType::DoubleValue(d)) = kept.value_type else {
274 panic!()
275 };
276 assert!(d == 0.0 && d.is_sign_negative(), "the stored -0.0 is kept");
277 let nan = run(&mut fields, "stored_nan", TransformType::Minimum(int(1)));
278 assert!(matches!(nan.value_type, Some(ValueType::DoubleValue(d)) if d.is_nan()));
279 assert_eq!(
280 run(&mut fields, "three", TransformType::Minimum(double(3.0))),
281 int(3)
282 );
283 assert_eq!(
284 run(&mut fields, "three", TransformType::Maximum(double(3.5))),
285 double(3.5)
286 );
287 }
288
289 #[test]
290 fn non_numeric_operands_are_rejected_with_the_official_message() {
291 for kind in [
292 TransformType::Increment(Value {
293 value_type: Some(ValueType::StringValue("x".into())),
294 }),
295 TransformType::Maximum(Value {
296 value_type: Some(ValueType::BooleanValue(true)),
297 }),
298 TransformType::Minimum(Value { value_type: None }),
299 ] {
300 let transform = FieldTransform {
301 field_path: "a".into(),
302 transform_type: Some(kind),
303 };
304 let err = apply(&mut Fields::new(), &transform, &Timestamp::default()).unwrap_err();
305 assert_eq!(err, "Input must be int64 or double.");
306 }
307 }
308
309 #[test]
310 fn array_operations_use_value_equality() {
311 let map = |v: Value| Value {
312 value_type: Some(ValueType::MapValue(MapValue {
313 fields: [("k".to_owned(), v)].into(),
314 })),
315 };
316 let mut fields: Fields = [(
317 "a".into(),
318 array(vec![int(1), map(int(1)), double(f64::NAN)]),
319 )]
320 .into();
321 let operands = ArrayValue {
322 values: vec![double(1.0), map(double(1.0)), double(f64::NAN), int(2)],
323 };
324 run(
325 &mut fields,
326 "a",
327 TransformType::AppendMissingElements(operands.clone()),
328 );
329 assert_eq!(array_elements(fields.get("a")).len(), 4, "only 2 is new");
330 run(
331 &mut fields,
332 "a",
333 TransformType::RemoveAllFromArray(operands),
334 );
335 assert!(array_elements(fields.get("a")).is_empty());
336 }
337}