1use crate::state::StateStore;
4use crate::types::{FieldChange, ObservedState, Op, Plan};
5use alembic_core::{
6 key_string, uid_v5, FieldType, JsonMap, Key, Object, Schema, TypeName, TypeSchema, Uid,
7};
8use serde_json::Value;
9use std::cmp::Reverse;
10use std::collections::{BTreeMap, BTreeSet, BinaryHeap};
11
12pub fn plan(
14 desired: &[Object],
15 observed: &ObservedState,
16 state: &StateStore,
17 schema: &alembic_core::Schema,
18 allow_delete: bool,
19) -> Plan {
20 let mut ops = Vec::new();
21 let mut matched = BTreeSet::new();
22 let mut backend_to_uid = BTreeMap::new();
23
24 for (type_name, mapping) in state.all_mappings() {
25 for (uid, backend_id) in mapping {
26 backend_to_uid.insert((backend_id.clone(), type_name.clone()), *uid);
27 }
28 }
29
30 let mut desired_sorted = desired.to_vec();
31 desired_sorted.sort_by_key(|a| op_sort_key(&a.type_name, &a.key));
32
33 for object in desired_sorted.iter() {
34 let observed_object = state
35 .backend_id(object.type_name.clone(), object.uid)
36 .and_then(|id| observed.by_backend_id.get(&(object.type_name.clone(), id)))
37 .or_else(|| {
38 observed
39 .by_key
40 .get(&(object.type_name.clone(), key_string(&object.key)))
41 });
42
43 if let Some(obs) = observed_object {
44 let changes = diff_object(obs, object);
45 if !changes.is_empty() {
46 ops.push(Op::Update {
47 uid: object.uid,
48 type_name: object.type_name.clone(),
49 desired: object.clone(),
50 changes,
51 backend_id: obs.backend_id.clone(),
52 });
53 }
54 if let Some(backend_id) = &obs.backend_id {
55 matched.insert(backend_id.clone());
56 }
57 } else {
58 ops.push(Op::Create {
59 uid: object.uid,
60 type_name: object.type_name.clone(),
61 desired: object.clone(),
62 });
63 }
64 }
65
66 if allow_delete {
67 for ((type_name, backend_id), obs) in &observed.by_backend_id {
68 if matched.contains(backend_id) {
69 continue;
70 }
71 let uid = backend_to_uid
72 .get(&(backend_id.clone(), type_name.clone()))
73 .copied()
74 .unwrap_or_else(|| uid_v5(type_name.as_str(), &key_string(&obs.key)));
75 ops.push(Op::Delete {
76 uid,
77 type_name: type_name.clone(),
78 key: obs.key.clone(),
79 backend_id: Some(backend_id.clone()),
80 });
81 }
82 }
83
84 ops.sort_by_key(op_order_key);
85
86 let mut plan = Plan {
87 schema: schema.clone(),
88 ops,
89 summary: None,
90 };
91 plan.summary = Some(plan.summary());
92 plan
93}
94
95fn diff_attrs(existing: &JsonMap, desired: &JsonMap) -> Vec<FieldChange> {
97 let mut changes = Vec::new();
98 let keys: BTreeSet<String> = existing.keys().chain(desired.keys()).cloned().collect();
99
100 for key in keys.iter() {
101 let from = existing.get(key).cloned().unwrap_or(Value::Null);
102 let desired_has = desired.contains_key(key);
103 if !desired_has {
104 continue;
105 }
106 let to = desired.get(key).cloned().unwrap_or(Value::Null);
107 if from != to {
108 changes.push(FieldChange {
109 field: key.clone(),
110 from,
111 to,
112 });
113 }
114 }
115
116 changes
117}
118
119fn diff_object(existing: &crate::types::ObservedObject, desired: &Object) -> Vec<FieldChange> {
120 diff_attrs(&existing.attrs, &desired.attrs)
121}
122
123fn op_sort_key(type_name: &TypeName, key: &Key) -> (String, String) {
125 (type_name.as_str().to_string(), key_string(key))
126}
127
128type OrderKey = (String, u8, String);
130
131fn op_order_key(op: &Op) -> OrderKey {
133 let (type_name, key, weight) = match op {
134 Op::Create {
135 type_name, desired, ..
136 } => (type_name.clone(), key_string(&desired.key), 0u8),
137 Op::Update {
138 type_name, desired, ..
139 } => (type_name.clone(), key_string(&desired.key), 1u8),
140 Op::Delete { type_name, key, .. } => (type_name.clone(), key_string(key), 2u8),
141 };
142 (type_name.as_str().to_string(), weight, key)
143}
144
145fn collect_refs_in_value(field_type: &FieldType, value: &Value, out: &mut BTreeSet<Uid>) {
148 match field_type {
149 FieldType::Ref { .. } => {
150 if let Some(uid) = value.as_str().and_then(|raw| Uid::parse_str(raw).ok()) {
151 out.insert(uid);
152 }
153 }
154 FieldType::ListRef { .. } => {
155 if let Value::Array(items) = value {
156 for item in items {
157 if let Some(uid) = item.as_str().and_then(|raw| Uid::parse_str(raw).ok()) {
158 out.insert(uid);
159 }
160 }
161 }
162 }
163 FieldType::List { item } => {
164 if let Value::Array(items) = value {
165 for item_value in items {
166 collect_refs_in_value(item, item_value, out);
167 }
168 }
169 }
170 FieldType::Map { value: inner } => {
171 if let Value::Object(map) = value {
172 for entry in map.values() {
173 collect_refs_in_value(inner, entry, out);
174 }
175 }
176 }
177 _ => {}
178 }
179}
180
181fn collect_referenced_uids(
185 type_schema: &TypeSchema,
186 attrs: &BTreeMap<String, Value>,
187) -> BTreeSet<Uid> {
188 let mut uids = BTreeSet::new();
189 for (field, value) in attrs {
190 if let Some(field_schema) = type_schema
191 .fields
192 .get(field)
193 .or_else(|| type_schema.key.get(field))
194 {
195 collect_refs_in_value(&field_schema.r#type, value, &mut uids);
196 }
197 }
198 uids
199}
200
201fn op_referenced_uids(op: &Op, schema: &Schema) -> BTreeSet<Uid> {
205 let Some(type_schema) = schema.types.get(op.type_name().as_str()) else {
206 return BTreeSet::new();
207 };
208 match op {
209 Op::Create { desired, .. } | Op::Update { desired, .. } => {
210 let mut uids = collect_referenced_uids(type_schema, &desired.attrs);
211 uids.extend(collect_referenced_uids(type_schema, &desired.key));
212 uids
213 }
214 Op::Delete { key, .. } => collect_referenced_uids(type_schema, key),
215 }
216}
217
218fn stable_toposort(ops: &[&Op], edges: &[BTreeSet<usize>]) -> Vec<usize> {
224 let n = ops.len();
225 let keys: Vec<OrderKey> = ops.iter().map(|&op| op_order_key(op)).collect();
226
227 let mut indegree = vec![0usize; n];
228 for succs in edges {
229 for &b in succs {
230 indegree[b] += 1;
231 }
232 }
233
234 let mut ready: BinaryHeap<Reverse<(OrderKey, usize)>> = BinaryHeap::new();
235 for (i, °) in indegree.iter().enumerate() {
236 if deg == 0 {
237 ready.push(Reverse((keys[i].clone(), i)));
238 }
239 }
240
241 let mut order = Vec::with_capacity(n);
242 while let Some(Reverse((_, i))) = ready.pop() {
243 order.push(i);
244 for &b in &edges[i] {
245 indegree[b] -= 1;
246 if indegree[b] == 0 {
247 ready.push(Reverse((keys[b].clone(), b)));
248 }
249 }
250 }
251
252 if order.len() < n {
255 let mut placed = vec![false; n];
256 for &i in &order {
257 placed[i] = true;
258 }
259 let mut remaining: Vec<usize> = (0..n).filter(|&i| !placed[i]).collect();
260 remaining.sort_by(|&a, &b| keys[a].cmp(&keys[b]).then(a.cmp(&b)));
261 order.extend(remaining);
262 }
263
264 order
265}
266
267fn ordered_by_refs(ops: &[&Op], schema: &Schema, reverse: bool) -> Vec<Op> {
272 let uid_to_node: BTreeMap<Uid, usize> = ops
273 .iter()
274 .enumerate()
275 .map(|(i, op)| (op.uid(), i))
276 .collect();
277
278 let mut edges: Vec<BTreeSet<usize>> = vec![BTreeSet::new(); ops.len()];
279 for (i, &op) in ops.iter().enumerate() {
280 for referenced in op_referenced_uids(op, schema) {
281 let Some(&j) = uid_to_node.get(&referenced) else {
282 continue;
283 };
284 if i == j {
285 continue;
286 }
287 if reverse {
288 edges[i].insert(j); } else {
290 edges[j].insert(i); }
292 }
293 }
294
295 stable_toposort(ops, &edges)
296 .into_iter()
297 .map(|i| ops[i].clone())
298 .collect()
299}
300
301pub fn sort_ops_for_apply(ops: &[Op], schema: &Schema) -> Vec<Op> {
306 let (non_deletes, deletes): (Vec<&Op>, Vec<&Op>) =
307 ops.iter().partition(|op| !matches!(op, Op::Delete { .. }));
308
309 let mut result = ordered_by_refs(&non_deletes, schema, false);
310 result.extend(ordered_by_refs(&deletes, schema, true));
311 result
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317 use crate::state::{StateData, StateStore};
318 use crate::types::{BackendId, ObservedObject, ObservedState};
319 use alembic_core::{
320 FieldSchema, FieldType, JsonMap, Key, Object, Schema, TypeName, TypeSchema, Uid,
321 };
322 use serde_json::json;
323 use std::collections::BTreeMap;
324
325 fn make_key(slug: &str) -> Key {
326 let mut k = BTreeMap::new();
327 k.insert("slug".to_string(), json!(slug));
328 Key::from(k)
329 }
330
331 fn make_attrs(pairs: &[(&str, serde_json::Value)]) -> JsonMap {
332 let mut m = BTreeMap::new();
333 for (k, v) in pairs {
334 m.insert(k.to_string(), v.clone());
335 }
336 JsonMap::from(m)
337 }
338
339 fn make_object(uid: u128, type_name: &str, slug: &str, attrs: JsonMap) -> Object {
340 Object::new(
341 Uid::from_u128(uid),
342 TypeName::new(type_name),
343 make_key(slug),
344 attrs,
345 )
346 .unwrap()
347 }
348
349 fn empty_schema() -> Schema {
350 Schema {
351 types: BTreeMap::new(),
352 }
353 }
354
355 fn empty_state() -> StateStore {
356 StateStore::new(None, StateData::default())
357 }
358
359 #[test]
362 fn diff_attrs_identical_maps() {
363 let attrs = make_attrs(&[("name", json!("FRA1"))]);
364 let changes = diff_attrs(&attrs, &attrs);
365 assert!(changes.is_empty());
366 }
367
368 #[test]
369 fn diff_attrs_field_changed() {
370 let existing = make_attrs(&[("name", json!("FRA1"))]);
371 let desired = make_attrs(&[("name", json!("FRA2"))]);
372 let changes = diff_attrs(&existing, &desired);
373 assert_eq!(changes.len(), 1);
374 assert_eq!(changes[0].field, "name");
375 assert_eq!(changes[0].from, json!("FRA1"));
376 assert_eq!(changes[0].to, json!("FRA2"));
377 }
378
379 #[test]
380 fn diff_attrs_field_added() {
381 let existing = make_attrs(&[]);
382 let desired = make_attrs(&[("name", json!("FRA1"))]);
383 let changes = diff_attrs(&existing, &desired);
384 assert_eq!(changes.len(), 1);
385 assert_eq!(changes[0].field, "name");
386 assert_eq!(changes[0].from, json!(null));
387 assert_eq!(changes[0].to, json!("FRA1"));
388 }
389
390 #[test]
391 fn diff_attrs_field_removed_in_desired_is_ignored() {
392 let existing = make_attrs(&[("name", json!("FRA1")), ("extra", json!(true))]);
393 let desired = make_attrs(&[("name", json!("FRA1"))]);
394 let changes = diff_attrs(&existing, &desired);
395 assert!(changes.is_empty());
396 }
397
398 #[test]
399 fn diff_attrs_multiple_changes() {
400 let existing = make_attrs(&[("a", json!(1)), ("b", json!(2))]);
401 let desired = make_attrs(&[("a", json!(10)), ("b", json!(20))]);
402 let changes = diff_attrs(&existing, &desired);
403 assert_eq!(changes.len(), 2);
404 let fields: Vec<&str> = changes.iter().map(|c| c.field.as_str()).collect();
405 assert!(fields.contains(&"a"));
406 assert!(fields.contains(&"b"));
407 }
408
409 #[test]
410 fn diff_attrs_type_change() {
411 let existing = make_attrs(&[("val", json!("string"))]);
412 let desired = make_attrs(&[("val", json!(42))]);
413 let changes = diff_attrs(&existing, &desired);
414 assert_eq!(changes.len(), 1);
415 assert_eq!(changes[0].from, json!("string"));
416 assert_eq!(changes[0].to, json!(42));
417 }
418
419 #[test]
422 fn plan_creates_for_new_objects() {
423 let desired = vec![make_object(
424 1,
425 "dcim.site",
426 "fra1",
427 make_attrs(&[("name", json!("FRA1"))]),
428 )];
429 let observed = ObservedState::default();
430 let result = plan(&desired, &observed, &empty_state(), &empty_schema(), false);
431 assert_eq!(result.ops.len(), 1);
432 assert!(matches!(&result.ops[0], Op::Create { uid, type_name, .. }
433 if *uid == Uid::from_u128(1) && type_name.as_str() == "dcim.site"));
434 let summary = result.summary.unwrap();
435 assert_eq!(summary.create, 1);
436 assert_eq!(summary.update, 0);
437 assert_eq!(summary.delete, 0);
438 }
439
440 #[test]
441 fn plan_updates_when_attrs_differ() {
442 let desired = vec![make_object(
443 1,
444 "dcim.site",
445 "fra1",
446 make_attrs(&[("name", json!("FRA2"))]),
447 )];
448 let mut observed = ObservedState::default();
449 observed
450 .insert(ObservedObject {
451 type_name: TypeName::new("dcim.site"),
452 key: make_key("fra1"),
453 attrs: make_attrs(&[("name", json!("FRA1"))]),
454 backend_id: Some(BackendId::Int(100)),
455 })
456 .unwrap();
457 let result = plan(&desired, &observed, &empty_state(), &empty_schema(), false);
458 assert_eq!(result.ops.len(), 1);
459 match &result.ops[0] {
460 Op::Update {
461 changes,
462 backend_id,
463 ..
464 } => {
465 assert_eq!(changes.len(), 1);
466 assert_eq!(changes[0].field, "name");
467 assert_eq!(backend_id, &Some(BackendId::Int(100)));
468 }
469 other => panic!("expected Update, got {:?}", other),
470 }
471 }
472
473 #[test]
474 fn plan_no_op_when_identical() {
475 let desired = vec![make_object(
476 1,
477 "dcim.site",
478 "fra1",
479 make_attrs(&[("name", json!("FRA1"))]),
480 )];
481 let mut observed = ObservedState::default();
482 observed
483 .insert(ObservedObject {
484 type_name: TypeName::new("dcim.site"),
485 key: make_key("fra1"),
486 attrs: make_attrs(&[("name", json!("FRA1"))]),
487 backend_id: Some(BackendId::Int(100)),
488 })
489 .unwrap();
490 let result = plan(&desired, &observed, &empty_state(), &empty_schema(), false);
491 assert!(result.ops.is_empty());
492 }
493
494 #[test]
495 fn plan_deletes_unmatched_when_allowed() {
496 let desired = vec![];
497 let mut observed = ObservedState::default();
498 observed
499 .insert(ObservedObject {
500 type_name: TypeName::new("dcim.site"),
501 key: make_key("fra1"),
502 attrs: make_attrs(&[("name", json!("FRA1"))]),
503 backend_id: Some(BackendId::Int(100)),
504 })
505 .unwrap();
506 let result = plan(&desired, &observed, &empty_state(), &empty_schema(), true);
507 assert_eq!(result.ops.len(), 1);
508 assert!(matches!(
509 &result.ops[0],
510 Op::Delete {
511 backend_id: Some(BackendId::Int(100)),
512 ..
513 }
514 ));
515 }
516
517 #[test]
518 fn plan_no_deletes_when_disallowed() {
519 let desired = vec![];
520 let mut observed = ObservedState::default();
521 observed
522 .insert(ObservedObject {
523 type_name: TypeName::new("dcim.site"),
524 key: make_key("fra1"),
525 attrs: make_attrs(&[("name", json!("FRA1"))]),
526 backend_id: Some(BackendId::Int(100)),
527 })
528 .unwrap();
529 let result = plan(&desired, &observed, &empty_state(), &empty_schema(), false);
530 assert!(result.ops.is_empty());
531 }
532
533 #[test]
534 fn plan_matched_objects_not_deleted() {
535 let desired = vec![make_object(
536 1,
537 "dcim.site",
538 "fra1",
539 make_attrs(&[("name", json!("FRA1"))]),
540 )];
541 let mut observed = ObservedState::default();
542 observed
543 .insert(ObservedObject {
544 type_name: TypeName::new("dcim.site"),
545 key: make_key("fra1"),
546 attrs: make_attrs(&[("name", json!("FRA1"))]),
547 backend_id: Some(BackendId::Int(100)),
548 })
549 .unwrap();
550 let result = plan(&desired, &observed, &empty_state(), &empty_schema(), true);
551 assert!(result.ops.is_empty());
552 }
553
554 #[test]
555 fn plan_mixed_create_update_delete() {
556 let desired = vec![
557 make_object(
558 1,
559 "dcim.site",
560 "fra1",
561 make_attrs(&[("name", json!("FRA1-new"))]),
562 ),
563 make_object(
564 2,
565 "dcim.site",
566 "ams1",
567 make_attrs(&[("name", json!("AMS1"))]),
568 ),
569 ];
570 let mut observed = ObservedState::default();
571 observed
572 .insert(ObservedObject {
573 type_name: TypeName::new("dcim.site"),
574 key: make_key("fra1"),
575 attrs: make_attrs(&[("name", json!("FRA1"))]),
576 backend_id: Some(BackendId::Int(100)),
577 })
578 .unwrap();
579 observed
580 .insert(ObservedObject {
581 type_name: TypeName::new("dcim.site"),
582 key: make_key("lhr1"),
583 attrs: make_attrs(&[("name", json!("LHR1"))]),
584 backend_id: Some(BackendId::Int(200)),
585 })
586 .unwrap();
587 let result = plan(&desired, &observed, &empty_state(), &empty_schema(), true);
588
589 let creates: Vec<_> = result
590 .ops
591 .iter()
592 .filter(|op| matches!(op, Op::Create { .. }))
593 .collect();
594 let updates: Vec<_> = result
595 .ops
596 .iter()
597 .filter(|op| matches!(op, Op::Update { .. }))
598 .collect();
599 let deletes: Vec<_> = result
600 .ops
601 .iter()
602 .filter(|op| matches!(op, Op::Delete { .. }))
603 .collect();
604 assert_eq!(creates.len(), 1);
605 assert_eq!(updates.len(), 1);
606 assert_eq!(deletes.len(), 1);
607
608 let summary = result.summary.unwrap();
609 assert_eq!(summary.create, 1);
610 assert_eq!(summary.update, 1);
611 assert_eq!(summary.delete, 1);
612 }
613
614 #[test]
615 fn plan_uses_state_mapping_for_lookup() {
616 let mut state_data = StateData::default();
617 state_data
618 .mappings
619 .entry(TypeName::new("dcim.site"))
620 .or_default()
621 .insert(Uid::from_u128(1), BackendId::Int(100));
622 let state = StateStore::new(None, state_data);
623
624 let desired = vec![make_object(
625 1,
626 "dcim.site",
627 "fra1",
628 make_attrs(&[("name", json!("FRA2"))]),
629 )];
630 let mut observed = ObservedState::default();
631 observed
632 .insert(ObservedObject {
633 type_name: TypeName::new("dcim.site"),
634 key: make_key("fra1"),
635 attrs: make_attrs(&[("name", json!("FRA1"))]),
636 backend_id: Some(BackendId::Int(100)),
637 })
638 .unwrap();
639 let result = plan(&desired, &observed, &state, &empty_schema(), false);
640 assert_eq!(result.ops.len(), 1);
641 assert!(matches!(&result.ops[0], Op::Update { .. }));
642 }
643
644 #[test]
647 fn sort_ops_creates_before_deletes() {
648 let ops = vec![
649 Op::Delete {
650 uid: Uid::from_u128(1),
651 type_name: TypeName::new("dcim.site"),
652 key: make_key("fra1"),
653 backend_id: Some(BackendId::Int(100)),
654 },
655 Op::Create {
656 uid: Uid::from_u128(2),
657 type_name: TypeName::new("dcim.site"),
658 desired: make_object(2, "dcim.site", "ams1", make_attrs(&[])),
659 },
660 ];
661 let sorted = sort_ops_for_apply(&ops, &empty_schema());
662 assert!(matches!(&sorted[0], Op::Create { .. }));
663 assert!(matches!(&sorted[1], Op::Delete { .. }));
664 }
665
666 #[test]
667 fn sort_ops_updates_before_deletes() {
668 let ops = vec![
669 Op::Delete {
670 uid: Uid::from_u128(1),
671 type_name: TypeName::new("dcim.site"),
672 key: make_key("fra1"),
673 backend_id: None,
674 },
675 Op::Update {
676 uid: Uid::from_u128(2),
677 type_name: TypeName::new("dcim.site"),
678 desired: make_object(2, "dcim.site", "ams1", make_attrs(&[])),
679 changes: vec![],
680 backend_id: None,
681 },
682 ];
683 let sorted = sort_ops_for_apply(&ops, &empty_schema());
684 assert!(matches!(&sorted[0], Op::Update { .. }));
685 assert!(matches!(&sorted[1], Op::Delete { .. }));
686 }
687
688 #[test]
689 fn sort_ops_deletes_last() {
690 let ops = vec![
691 Op::Delete {
692 uid: Uid::from_u128(1),
693 type_name: TypeName::new("a.type"),
694 key: make_key("a"),
695 backend_id: None,
696 },
697 Op::Delete {
698 uid: Uid::from_u128(2),
699 type_name: TypeName::new("z.type"),
700 key: make_key("z"),
701 backend_id: None,
702 },
703 ];
704 let sorted = sort_ops_for_apply(&ops, &empty_schema());
705 assert!(
707 matches!(&sorted[0], Op::Delete { type_name, .. } if type_name.as_str() == "a.type")
708 );
709 assert!(
710 matches!(&sorted[1], Op::Delete { type_name, .. } if type_name.as_str() == "z.type")
711 );
712 }
713
714 #[test]
715 fn sort_ops_empty_input() {
716 let sorted = sort_ops_for_apply(&[], &empty_schema());
717 assert!(sorted.is_empty());
718 }
719
720 #[test]
721 fn sort_ops_preserves_create_update_order() {
722 let ops = vec![
723 Op::Update {
724 uid: Uid::from_u128(2),
725 type_name: TypeName::new("dcim.site"),
726 desired: make_object(2, "dcim.site", "ams1", make_attrs(&[])),
727 changes: vec![],
728 backend_id: None,
729 },
730 Op::Create {
731 uid: Uid::from_u128(1),
732 type_name: TypeName::new("dcim.site"),
733 desired: make_object(1, "dcim.site", "aaa1", make_attrs(&[])),
734 },
735 ];
736 let sorted = sort_ops_for_apply(&ops, &empty_schema());
737 assert!(matches!(&sorted[0], Op::Create { .. }));
738 assert!(matches!(&sorted[1], Op::Update { .. }));
739 }
740
741 fn field(r#type: FieldType) -> FieldSchema {
744 FieldSchema {
745 r#type,
746 required: false,
747 nullable: true,
748 format: None,
749 pattern: None,
750 description: None,
751 }
752 }
753
754 fn ref_t() -> FieldType {
755 FieldType::Ref {
756 target: "node".to_string(),
757 }
758 }
759
760 fn list_ref_t() -> FieldType {
761 FieldType::ListRef {
762 target: "node".to_string(),
763 }
764 }
765
766 fn node_schema(fields: &[(&str, FieldType)]) -> Schema {
768 let mut field_map = BTreeMap::new();
769 for (name, ty) in fields {
770 field_map.insert(name.to_string(), field(ty.clone()));
771 }
772 let mut key = BTreeMap::new();
773 key.insert("slug".to_string(), field(FieldType::Slug));
774 let mut types = BTreeMap::new();
775 types.insert(
776 "node".to_string(),
777 TypeSchema {
778 key,
779 fields: field_map,
780 },
781 );
782 Schema { types }
783 }
784
785 fn uref(uid: u128) -> serde_json::Value {
787 json!(Uid::from_u128(uid).to_string())
788 }
789
790 fn create_node(uid: u128, slug: &str, attrs: JsonMap) -> Op {
791 Op::Create {
792 uid: Uid::from_u128(uid),
793 type_name: TypeName::new("node"),
794 desired: make_object(uid, "node", slug, attrs),
795 }
796 }
797
798 fn order_index(ops: &[Op], uid: u128) -> usize {
800 ops.iter()
801 .position(|op| op.uid().as_u128() == uid)
802 .expect("uid present in ordered ops")
803 }
804
805 #[test]
806 fn toposort_linear_ref_chain() {
807 let schema = node_schema(&[("next", ref_t())]);
808 let ops = vec![
809 create_node(1, "a", make_attrs(&[("next", uref(2))])),
810 create_node(2, "b", make_attrs(&[("next", uref(3))])),
811 create_node(3, "c", make_attrs(&[])),
812 ];
813 let sorted = sort_ops_for_apply(&ops, &schema);
814 assert!(order_index(&sorted, 3) < order_index(&sorted, 2));
816 assert!(order_index(&sorted, 2) < order_index(&sorted, 1));
817 }
818
819 #[test]
820 fn toposort_diamond_shared_dep() {
821 let schema = node_schema(&[("parent", ref_t()), ("left", ref_t()), ("right", ref_t())]);
822 let ops = vec![
823 create_node(1, "d", make_attrs(&[("left", uref(2)), ("right", uref(3))])),
824 create_node(2, "b", make_attrs(&[("parent", uref(4))])),
825 create_node(3, "c", make_attrs(&[("parent", uref(4))])),
826 create_node(4, "a", make_attrs(&[])),
827 ];
828 let sorted = sort_ops_for_apply(&ops, &schema);
829 let (a, b, c, d) = (
830 order_index(&sorted, 4),
831 order_index(&sorted, 2),
832 order_index(&sorted, 3),
833 order_index(&sorted, 1),
834 );
835 assert!(a < b && a < c, "shared dep created first");
836 assert!(b < d && c < d, "both branches created before the joiner");
837 }
838
839 #[test]
840 fn toposort_list_ref_fan_out() {
841 let schema = node_schema(&[("members", list_ref_t())]);
842 let ops = vec![
843 create_node(
844 1,
845 "a",
846 make_attrs(&[("members", json!([uref(2), uref(3), uref(4)]))]),
847 ),
848 create_node(2, "b", make_attrs(&[])),
849 create_node(3, "c", make_attrs(&[])),
850 create_node(4, "d", make_attrs(&[])),
851 ];
852 let sorted = sort_ops_for_apply(&ops, &schema);
853 let a = order_index(&sorted, 1);
854 assert!(order_index(&sorted, 2) < a);
855 assert!(order_index(&sorted, 3) < a);
856 assert!(order_index(&sorted, 4) < a);
857 }
858
859 #[test]
860 fn toposort_refs_nested_in_list_and_map() {
861 let schema = node_schema(&[
862 (
863 "group",
864 FieldType::List {
865 item: Box::new(ref_t()),
866 },
867 ),
868 (
869 "lookup",
870 FieldType::Map {
871 value: Box::new(ref_t()),
872 },
873 ),
874 ]);
875 let ops = vec![
876 create_node(
877 1,
878 "a",
879 make_attrs(&[
880 ("group", json!([uref(2)])),
881 ("lookup", json!({ "k": uref(3) })),
882 ]),
883 ),
884 create_node(2, "b", make_attrs(&[])),
885 create_node(3, "c", make_attrs(&[])),
886 ];
887 let sorted = sort_ops_for_apply(&ops, &schema);
888 let a = order_index(&sorted, 1);
889 assert!(order_index(&sorted, 2) < a, "ref nested in list");
890 assert!(order_index(&sorted, 3) < a, "ref nested in map");
891 }
892
893 #[test]
894 fn toposort_reference_cycle_falls_back_to_stable_order() {
895 let schema = node_schema(&[("next", ref_t())]);
896 let ops = vec![
897 create_node(1, "aaa", make_attrs(&[("next", uref(2))])),
898 create_node(2, "bbb", make_attrs(&[("next", uref(1))])),
899 ];
900 let sorted = sort_ops_for_apply(&ops, &schema);
901 assert_eq!(sorted.len(), 2);
903 assert_eq!(order_index(&sorted, 1), 0);
905 assert_eq!(order_index(&sorted, 2), 1);
906 }
907
908 #[test]
909 fn reverse_toposort_deletes_child_before_parent() {
910 let mut node_key = BTreeMap::new();
913 node_key.insert("slug".to_string(), field(FieldType::Slug));
914 let mut child_key = BTreeMap::new();
915 child_key.insert("parent".to_string(), field(ref_t()));
916 child_key.insert("slug".to_string(), field(FieldType::Slug));
917 let mut types = BTreeMap::new();
918 types.insert(
919 "node".to_string(),
920 TypeSchema {
921 key: node_key,
922 fields: BTreeMap::new(),
923 },
924 );
925 types.insert(
926 "child".to_string(),
927 TypeSchema {
928 key: child_key,
929 fields: BTreeMap::new(),
930 },
931 );
932 let schema = Schema { types };
933
934 let parent_key = Key::from(BTreeMap::from([("slug".to_string(), json!("p"))]));
935 let child_key_val = Key::from(BTreeMap::from([
936 ("parent".to_string(), uref(1)),
937 ("slug".to_string(), json!("c")),
938 ]));
939 let ops = vec![
940 Op::Delete {
941 uid: Uid::from_u128(1),
942 type_name: TypeName::new("node"),
943 key: parent_key,
944 backend_id: None,
945 },
946 Op::Delete {
947 uid: Uid::from_u128(2),
948 type_name: TypeName::new("child"),
949 key: child_key_val,
950 backend_id: None,
951 },
952 ];
953 let sorted = sort_ops_for_apply(&ops, &schema);
954 assert!(
955 order_index(&sorted, 2) < order_index(&sorted, 1),
956 "child deleted before the parent it references"
957 );
958 assert!(sorted.iter().all(|op| matches!(op, Op::Delete { .. })));
959 }
960
961 #[test]
962 fn toposort_keeps_deletes_after_creates_and_updates() {
963 let schema = node_schema(&[("next", ref_t())]);
964 let ops = vec![
965 Op::Delete {
966 uid: Uid::from_u128(9),
967 type_name: TypeName::new("node"),
968 key: make_key("z"),
969 backend_id: None,
970 },
971 create_node(1, "a", make_attrs(&[("next", uref(2))])),
972 create_node(2, "b", make_attrs(&[])),
973 ];
974 let sorted = sort_ops_for_apply(&ops, &schema);
975 let non_delete_count = sorted
976 .iter()
977 .filter(|op| !matches!(op, Op::Delete { .. }))
978 .count();
979 let delete_pos = sorted
980 .iter()
981 .position(|op| matches!(op, Op::Delete { .. }))
982 .unwrap();
983 assert_eq!(delete_pos, non_delete_count);
985 assert!(order_index(&sorted, 2) < order_index(&sorted, 1));
987 }
988}