1#[cfg(test)]
14use std::collections::{HashMap, HashSet};
15#[cfg(test)]
16use std::sync::Mutex;
17
18use crate::error::{ArrayError, ArrayResult};
19use crate::sync::hlc::Hlc;
20use crate::sync::op::{ArrayOp, ArrayOpKind};
21use crate::types::coord::value::CoordValue;
22
23#[derive(Clone, Debug, PartialEq)]
27pub enum ApplyOutcome {
28 Idempotent,
30 Applied,
32 Rejected(ApplyRejection),
34}
35
36#[derive(Clone, Debug, PartialEq)]
38pub enum ApplyRejection {
39 SchemaTooNew {
43 local: Hlc,
45 op: Hlc,
47 },
48 ArrayUnknown {
52 name: String,
54 },
55 ShapeInvalid {
57 detail: String,
59 },
60 EngineRejected {
67 detail: String,
69 },
70}
71
72pub trait ApplyEngine {
80 fn schema_hlc(&self, array: &str) -> ArrayResult<Option<Hlc>>;
83
84 fn already_seen(&self, array: &str, hlc: Hlc) -> ArrayResult<bool>;
88
89 fn apply_put(&mut self, op: &ArrayOp) -> ArrayResult<()>;
91
92 fn apply_delete(&mut self, op: &ArrayOp) -> ArrayResult<()>;
94
95 fn apply_erase(&mut self, op: &ArrayOp) -> ArrayResult<()>;
97
98 fn invalidate_tile(&mut self, array: &str, coord: &[CoordValue]) -> ArrayResult<()>;
103}
104
105pub fn apply_op<E: ApplyEngine>(engine: &mut E, op: &ArrayOp) -> ArrayResult<ApplyOutcome> {
121 if let Err(e) = op.validate_shape() {
123 return Ok(ApplyOutcome::Rejected(ApplyRejection::ShapeInvalid {
124 detail: e.to_string(),
125 }));
126 }
127
128 match engine.schema_hlc(&op.header.array)? {
130 None => {
131 return Ok(ApplyOutcome::Rejected(ApplyRejection::ArrayUnknown {
132 name: op.header.array.clone(),
133 }));
134 }
135 Some(local_schema) if op.header.schema_hlc.is_later_than(&local_schema) => {
140 return Ok(ApplyOutcome::Rejected(ApplyRejection::SchemaTooNew {
141 local: local_schema,
142 op: op.header.schema_hlc,
143 }));
144 }
145 Some(_) => {}
146 }
147
148 if engine.already_seen(&op.header.array, op.header.hlc)? {
150 return Ok(ApplyOutcome::Idempotent);
151 }
152
153 let dispatch_result = match op.kind {
155 ArrayOpKind::Put => engine.apply_put(op),
156 ArrayOpKind::Delete => engine.apply_delete(op),
157 ArrayOpKind::Erase => engine.apply_erase(op),
158 };
159
160 if let Err(e) = dispatch_result {
161 match &e {
163 ArrayError::SegmentCorruption { .. } | ArrayError::HlcLockPoisoned => return Err(e),
164 _ => {
165 return Ok(ApplyOutcome::Rejected(ApplyRejection::EngineRejected {
166 detail: e.to_string(),
167 }));
168 }
169 }
170 }
171
172 engine.invalidate_tile(&op.header.array, &op.coord)?;
174
175 Ok(ApplyOutcome::Applied)
177}
178
179#[cfg(test)]
190pub struct MockEngine {
191 schemas: HashMap<String, Hlc>,
192 seen: HashSet<(String, [u8; 18])>,
193 pub applied: Vec<ArrayOp>,
194 pub invalidated: Vec<(String, Vec<CoordValue>)>,
195 reject_next_with: Mutex<Option<ArrayError>>,
196}
197
198#[cfg(test)]
199impl MockEngine {
200 pub fn new() -> Self {
202 Self {
203 schemas: HashMap::new(),
204 seen: HashSet::new(),
205 applied: Vec::new(),
206 invalidated: Vec::new(),
207 reject_next_with: Mutex::new(None),
208 }
209 }
210
211 pub fn register_array(&mut self, array: &str, schema_hlc: Hlc) {
213 self.schemas.insert(array.to_string(), schema_hlc);
214 }
215
216 pub fn set_reject_next(&self, err: ArrayError) {
218 *self
219 .reject_next_with
220 .lock()
221 .expect("invariant: MockEngine mutex is not poisoned") = Some(err);
222 }
223
224 fn take_inject(&self) -> Option<ArrayError> {
225 self.reject_next_with
226 .lock()
227 .expect("invariant: MockEngine mutex is not poisoned")
228 .take()
229 }
230}
231
232#[cfg(test)]
233impl Default for MockEngine {
234 fn default() -> Self {
235 Self::new()
236 }
237}
238
239#[cfg(test)]
240impl ApplyEngine for MockEngine {
241 fn schema_hlc(&self, array: &str) -> ArrayResult<Option<Hlc>> {
242 Ok(self.schemas.get(array).copied())
243 }
244
245 fn already_seen(&self, array: &str, hlc: Hlc) -> ArrayResult<bool> {
246 Ok(self.seen.contains(&(array.to_string(), hlc.to_bytes())))
247 }
248
249 fn apply_put(&mut self, op: &ArrayOp) -> ArrayResult<()> {
250 if let Some(err) = self.take_inject() {
251 return Err(err);
252 }
253 self.seen
254 .insert((op.header.array.clone(), op.header.hlc.to_bytes()));
255 self.applied.push(op.clone());
256 Ok(())
257 }
258
259 fn apply_delete(&mut self, op: &ArrayOp) -> ArrayResult<()> {
260 if let Some(err) = self.take_inject() {
261 return Err(err);
262 }
263 self.seen
264 .insert((op.header.array.clone(), op.header.hlc.to_bytes()));
265 self.applied.push(op.clone());
266 Ok(())
267 }
268
269 fn apply_erase(&mut self, op: &ArrayOp) -> ArrayResult<()> {
270 if let Some(err) = self.take_inject() {
271 return Err(err);
272 }
273 self.seen
274 .insert((op.header.array.clone(), op.header.hlc.to_bytes()));
275 self.applied.push(op.clone());
276 Ok(())
277 }
278
279 fn invalidate_tile(&mut self, array: &str, coord: &[CoordValue]) -> ArrayResult<()> {
280 self.invalidated.push((array.to_string(), coord.to_vec()));
281 Ok(())
282 }
283}
284
285#[cfg(test)]
288mod tests {
289 use super::*;
290 use crate::sync::hlc::Hlc;
291 use crate::sync::op::{ArrayOpHeader, ArrayOpKind};
292 use crate::sync::replica_id::ReplicaId;
293 use crate::types::cell_value::value::CellValue;
294 use crate::types::coord::value::CoordValue;
295
296 fn replica() -> ReplicaId {
297 ReplicaId::new(42)
298 }
299
300 fn hlc(ms: u64) -> Hlc {
301 Hlc::new(ms, 0, replica()).unwrap()
302 }
303
304 fn header(array: &str, op_ms: u64, schema_ms: u64) -> ArrayOpHeader {
305 ArrayOpHeader {
306 array: array.into(),
307 hlc: hlc(op_ms),
308 schema_hlc: hlc(schema_ms),
309 valid_from_ms: 0,
310 valid_until_ms: -1,
311 system_from_ms: op_ms as i64,
312 }
313 }
314
315 fn put_op(array: &str, op_ms: u64, schema_ms: u64) -> ArrayOp {
316 ArrayOp {
317 header: header(array, op_ms, schema_ms),
318 kind: ArrayOpKind::Put,
319 coord: vec![CoordValue::Int64(1)],
320 attrs: Some(vec![CellValue::Null]),
321 }
322 }
323
324 fn delete_op(array: &str, op_ms: u64, schema_ms: u64) -> ArrayOp {
325 ArrayOp {
326 header: header(array, op_ms, schema_ms),
327 kind: ArrayOpKind::Delete,
328 coord: vec![CoordValue::Int64(1)],
329 attrs: None,
330 }
331 }
332
333 #[test]
334 fn apply_put_succeeds() {
335 let mut engine = MockEngine::new();
336 engine.register_array("a", hlc(100));
337
338 let op = put_op("a", 50, 100);
339 let outcome = apply_op(&mut engine, &op).unwrap();
340 assert_eq!(outcome, ApplyOutcome::Applied);
341 assert_eq!(engine.applied.len(), 1);
342 assert_eq!(engine.invalidated.len(), 1);
343 }
344
345 #[test]
346 fn apply_idempotent_on_replay() {
347 let mut engine = MockEngine::new();
348 engine.register_array("a", hlc(100));
349
350 let op = put_op("a", 50, 100);
351 apply_op(&mut engine, &op).unwrap();
352 let outcome = apply_op(&mut engine, &op).unwrap();
353 assert_eq!(outcome, ApplyOutcome::Idempotent);
354 assert_eq!(engine.applied.len(), 1);
356 }
357
358 #[test]
359 fn apply_rejects_unknown_array() {
360 let mut engine = MockEngine::new();
361 let op = put_op("unknown", 50, 100);
362 let outcome = apply_op(&mut engine, &op).unwrap();
363 assert!(matches!(
364 outcome,
365 ApplyOutcome::Rejected(ApplyRejection::ArrayUnknown { name }) if name == "unknown"
366 ));
367 }
368
369 #[test]
370 fn apply_rejects_schema_too_new() {
371 let mut engine = MockEngine::new();
372 engine.register_array("a", hlc(50)); let op = put_op("a", 10, 100); let outcome = apply_op(&mut engine, &op).unwrap();
375 assert!(matches!(
376 outcome,
377 ApplyOutcome::Rejected(ApplyRejection::SchemaTooNew { local, op: op_hlc })
378 if local == hlc(50) && op_hlc == hlc(100)
379 ));
380 }
381
382 #[test]
388 fn same_instant_schema_is_admitted_regardless_of_replica_id() {
389 for (local_replica, op_replica) in [(1u64, 9u64), (9, 1)] {
390 let local_schema = Hlc::new(50, 0, ReplicaId::new(local_replica)).unwrap();
391 let op_schema = Hlc::new(50, 0, ReplicaId::new(op_replica)).unwrap();
392
393 let mut engine = MockEngine::new();
394 engine.register_array("a", local_schema);
395 let mut op = put_op("a", 10, 100);
396 op.header.schema_hlc = op_schema;
397
398 let outcome = apply_op(&mut engine, &op).unwrap();
399 assert_eq!(
400 outcome,
401 ApplyOutcome::Applied,
402 "same-instant schema must apply (local replica {local_replica}, \
403 op replica {op_replica}); the replica_id tiebreak must not gate admission"
404 );
405 }
406 }
407
408 #[test]
409 fn apply_rejects_invalid_shape() {
410 let mut engine = MockEngine::new();
411 engine.register_array("a", hlc(100));
412 let op = ArrayOp {
414 header: header("a", 50, 100),
415 kind: ArrayOpKind::Put,
416 coord: vec![CoordValue::Int64(1)],
417 attrs: None,
418 };
419 let outcome = apply_op(&mut engine, &op).unwrap();
420 assert!(matches!(
421 outcome,
422 ApplyOutcome::Rejected(ApplyRejection::ShapeInvalid { .. })
423 ));
424 }
425
426 #[test]
427 fn apply_wraps_engine_error_as_rejection() {
428 let mut engine = MockEngine::new();
429 engine.register_array("a", hlc(100));
430 engine.set_reject_next(ArrayError::InvalidOp {
431 detail: "simulated engine error".into(),
432 });
433 let op = put_op("a", 50, 100);
434 let outcome = apply_op(&mut engine, &op).unwrap();
435 assert!(matches!(
436 outcome,
437 ApplyOutcome::Rejected(ApplyRejection::EngineRejected { .. })
438 ));
439 assert!(engine.invalidated.is_empty());
441 }
442
443 #[test]
444 fn apply_invalidates_tile_after_success() {
445 let mut engine = MockEngine::new();
446 engine.register_array("b", hlc(100));
447 let op = delete_op("b", 30, 50);
448 apply_op(&mut engine, &op).unwrap();
449 assert_eq!(engine.invalidated.len(), 1);
450 assert_eq!(engine.invalidated[0].0, "b");
451 }
452}