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