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((object.type_name.clone(), 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(&(type_name.clone(), backend_id.clone())) {
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_deletes_cross_type_id_collision() {
556 let desired = vec![make_object(
559 1,
560 "dcim.site",
561 "fra1",
562 make_attrs(&[("name", json!("FRA1"))]),
563 )];
564 let mut observed = ObservedState::default();
565 observed
566 .insert(ObservedObject {
567 type_name: TypeName::new("dcim.site"),
568 key: make_key("fra1"),
569 attrs: make_attrs(&[("name", json!("FRA1"))]),
570 backend_id: Some(BackendId::Int(100)),
571 })
572 .unwrap();
573 observed
574 .insert(ObservedObject {
575 type_name: TypeName::new("dcim.device"),
576 key: make_key("leaf01"),
577 attrs: make_attrs(&[]),
578 backend_id: Some(BackendId::Int(100)),
579 })
580 .unwrap();
581 let result = plan(&desired, &observed, &empty_state(), &empty_schema(), true);
582 let deletes: Vec<&Op> = result
583 .ops
584 .iter()
585 .filter(|op| matches!(op, Op::Delete { .. }))
586 .collect();
587 assert_eq!(deletes.len(), 1);
588 assert!(matches!(
589 deletes[0],
590 Op::Delete { type_name, .. } if type_name.as_str() == "dcim.device"
591 ));
592 }
593
594 #[test]
595 fn plan_mixed_create_update_delete() {
596 let desired = vec![
597 make_object(
598 1,
599 "dcim.site",
600 "fra1",
601 make_attrs(&[("name", json!("FRA1-new"))]),
602 ),
603 make_object(
604 2,
605 "dcim.site",
606 "ams1",
607 make_attrs(&[("name", json!("AMS1"))]),
608 ),
609 ];
610 let mut observed = ObservedState::default();
611 observed
612 .insert(ObservedObject {
613 type_name: TypeName::new("dcim.site"),
614 key: make_key("fra1"),
615 attrs: make_attrs(&[("name", json!("FRA1"))]),
616 backend_id: Some(BackendId::Int(100)),
617 })
618 .unwrap();
619 observed
620 .insert(ObservedObject {
621 type_name: TypeName::new("dcim.site"),
622 key: make_key("lhr1"),
623 attrs: make_attrs(&[("name", json!("LHR1"))]),
624 backend_id: Some(BackendId::Int(200)),
625 })
626 .unwrap();
627 let result = plan(&desired, &observed, &empty_state(), &empty_schema(), true);
628
629 let creates: Vec<_> = result
630 .ops
631 .iter()
632 .filter(|op| matches!(op, Op::Create { .. }))
633 .collect();
634 let updates: Vec<_> = result
635 .ops
636 .iter()
637 .filter(|op| matches!(op, Op::Update { .. }))
638 .collect();
639 let deletes: Vec<_> = result
640 .ops
641 .iter()
642 .filter(|op| matches!(op, Op::Delete { .. }))
643 .collect();
644 assert_eq!(creates.len(), 1);
645 assert_eq!(updates.len(), 1);
646 assert_eq!(deletes.len(), 1);
647
648 let summary = result.summary.unwrap();
649 assert_eq!(summary.create, 1);
650 assert_eq!(summary.update, 1);
651 assert_eq!(summary.delete, 1);
652 }
653
654 #[test]
655 fn plan_uses_state_mapping_for_lookup() {
656 let mut state_data = StateData::default();
657 state_data
658 .mappings
659 .entry(TypeName::new("dcim.site"))
660 .or_default()
661 .insert(Uid::from_u128(1), BackendId::Int(100));
662 let state = StateStore::new(None, state_data);
663
664 let desired = vec![make_object(
665 1,
666 "dcim.site",
667 "fra1",
668 make_attrs(&[("name", json!("FRA2"))]),
669 )];
670 let mut observed = ObservedState::default();
671 observed
672 .insert(ObservedObject {
673 type_name: TypeName::new("dcim.site"),
674 key: make_key("fra1"),
675 attrs: make_attrs(&[("name", json!("FRA1"))]),
676 backend_id: Some(BackendId::Int(100)),
677 })
678 .unwrap();
679 let result = plan(&desired, &observed, &state, &empty_schema(), false);
680 assert_eq!(result.ops.len(), 1);
681 assert!(matches!(&result.ops[0], Op::Update { .. }));
682 }
683
684 #[test]
687 fn sort_ops_creates_before_deletes() {
688 let ops = vec![
689 Op::Delete {
690 uid: Uid::from_u128(1),
691 type_name: TypeName::new("dcim.site"),
692 key: make_key("fra1"),
693 backend_id: Some(BackendId::Int(100)),
694 },
695 Op::Create {
696 uid: Uid::from_u128(2),
697 type_name: TypeName::new("dcim.site"),
698 desired: make_object(2, "dcim.site", "ams1", make_attrs(&[])),
699 },
700 ];
701 let sorted = sort_ops_for_apply(&ops, &empty_schema());
702 assert!(matches!(&sorted[0], Op::Create { .. }));
703 assert!(matches!(&sorted[1], Op::Delete { .. }));
704 }
705
706 #[test]
707 fn sort_ops_updates_before_deletes() {
708 let ops = vec![
709 Op::Delete {
710 uid: Uid::from_u128(1),
711 type_name: TypeName::new("dcim.site"),
712 key: make_key("fra1"),
713 backend_id: None,
714 },
715 Op::Update {
716 uid: Uid::from_u128(2),
717 type_name: TypeName::new("dcim.site"),
718 desired: make_object(2, "dcim.site", "ams1", make_attrs(&[])),
719 changes: vec![],
720 backend_id: None,
721 },
722 ];
723 let sorted = sort_ops_for_apply(&ops, &empty_schema());
724 assert!(matches!(&sorted[0], Op::Update { .. }));
725 assert!(matches!(&sorted[1], Op::Delete { .. }));
726 }
727
728 #[test]
729 fn sort_ops_deletes_last() {
730 let ops = vec![
731 Op::Delete {
732 uid: Uid::from_u128(1),
733 type_name: TypeName::new("a.type"),
734 key: make_key("a"),
735 backend_id: None,
736 },
737 Op::Delete {
738 uid: Uid::from_u128(2),
739 type_name: TypeName::new("z.type"),
740 key: make_key("z"),
741 backend_id: None,
742 },
743 ];
744 let sorted = sort_ops_for_apply(&ops, &empty_schema());
745 assert!(
747 matches!(&sorted[0], Op::Delete { type_name, .. } if type_name.as_str() == "a.type")
748 );
749 assert!(
750 matches!(&sorted[1], Op::Delete { type_name, .. } if type_name.as_str() == "z.type")
751 );
752 }
753
754 #[test]
755 fn sort_ops_empty_input() {
756 let sorted = sort_ops_for_apply(&[], &empty_schema());
757 assert!(sorted.is_empty());
758 }
759
760 #[test]
761 fn sort_ops_preserves_create_update_order() {
762 let ops = vec![
763 Op::Update {
764 uid: Uid::from_u128(2),
765 type_name: TypeName::new("dcim.site"),
766 desired: make_object(2, "dcim.site", "ams1", make_attrs(&[])),
767 changes: vec![],
768 backend_id: None,
769 },
770 Op::Create {
771 uid: Uid::from_u128(1),
772 type_name: TypeName::new("dcim.site"),
773 desired: make_object(1, "dcim.site", "aaa1", make_attrs(&[])),
774 },
775 ];
776 let sorted = sort_ops_for_apply(&ops, &empty_schema());
777 assert!(matches!(&sorted[0], Op::Create { .. }));
778 assert!(matches!(&sorted[1], Op::Update { .. }));
779 }
780
781 fn field(r#type: FieldType) -> FieldSchema {
784 FieldSchema {
785 r#type,
786 required: false,
787 nullable: true,
788 format: None,
789 pattern: None,
790 description: None,
791 }
792 }
793
794 fn ref_t() -> FieldType {
795 FieldType::Ref {
796 target: "node".to_string(),
797 }
798 }
799
800 fn list_ref_t() -> FieldType {
801 FieldType::ListRef {
802 target: "node".to_string(),
803 }
804 }
805
806 fn node_schema(fields: &[(&str, FieldType)]) -> Schema {
808 let mut field_map = BTreeMap::new();
809 for (name, ty) in fields {
810 field_map.insert(name.to_string(), field(ty.clone()));
811 }
812 let mut key = BTreeMap::new();
813 key.insert("slug".to_string(), field(FieldType::Slug));
814 let mut types = BTreeMap::new();
815 types.insert(
816 "node".to_string(),
817 TypeSchema {
818 key,
819 fields: field_map,
820 },
821 );
822 Schema { types }
823 }
824
825 fn uref(uid: u128) -> serde_json::Value {
827 json!(Uid::from_u128(uid).to_string())
828 }
829
830 fn create_node(uid: u128, slug: &str, attrs: JsonMap) -> Op {
831 Op::Create {
832 uid: Uid::from_u128(uid),
833 type_name: TypeName::new("node"),
834 desired: make_object(uid, "node", slug, attrs),
835 }
836 }
837
838 fn order_index(ops: &[Op], uid: u128) -> usize {
840 ops.iter()
841 .position(|op| op.uid().as_u128() == uid)
842 .expect("uid present in ordered ops")
843 }
844
845 #[test]
846 fn toposort_linear_ref_chain() {
847 let schema = node_schema(&[("next", ref_t())]);
848 let ops = vec![
849 create_node(1, "a", make_attrs(&[("next", uref(2))])),
850 create_node(2, "b", make_attrs(&[("next", uref(3))])),
851 create_node(3, "c", make_attrs(&[])),
852 ];
853 let sorted = sort_ops_for_apply(&ops, &schema);
854 assert!(order_index(&sorted, 3) < order_index(&sorted, 2));
856 assert!(order_index(&sorted, 2) < order_index(&sorted, 1));
857 }
858
859 #[test]
860 fn toposort_diamond_shared_dep() {
861 let schema = node_schema(&[("parent", ref_t()), ("left", ref_t()), ("right", ref_t())]);
862 let ops = vec![
863 create_node(1, "d", make_attrs(&[("left", uref(2)), ("right", uref(3))])),
864 create_node(2, "b", make_attrs(&[("parent", uref(4))])),
865 create_node(3, "c", make_attrs(&[("parent", uref(4))])),
866 create_node(4, "a", make_attrs(&[])),
867 ];
868 let sorted = sort_ops_for_apply(&ops, &schema);
869 let (a, b, c, d) = (
870 order_index(&sorted, 4),
871 order_index(&sorted, 2),
872 order_index(&sorted, 3),
873 order_index(&sorted, 1),
874 );
875 assert!(a < b && a < c, "shared dep created first");
876 assert!(b < d && c < d, "both branches created before the joiner");
877 }
878
879 #[test]
880 fn toposort_list_ref_fan_out() {
881 let schema = node_schema(&[("members", list_ref_t())]);
882 let ops = vec![
883 create_node(
884 1,
885 "a",
886 make_attrs(&[("members", json!([uref(2), uref(3), uref(4)]))]),
887 ),
888 create_node(2, "b", make_attrs(&[])),
889 create_node(3, "c", make_attrs(&[])),
890 create_node(4, "d", make_attrs(&[])),
891 ];
892 let sorted = sort_ops_for_apply(&ops, &schema);
893 let a = order_index(&sorted, 1);
894 assert!(order_index(&sorted, 2) < a);
895 assert!(order_index(&sorted, 3) < a);
896 assert!(order_index(&sorted, 4) < a);
897 }
898
899 #[test]
900 fn toposort_refs_nested_in_list_and_map() {
901 let schema = node_schema(&[
902 (
903 "group",
904 FieldType::List {
905 item: Box::new(ref_t()),
906 },
907 ),
908 (
909 "lookup",
910 FieldType::Map {
911 value: Box::new(ref_t()),
912 },
913 ),
914 ]);
915 let ops = vec![
916 create_node(
917 1,
918 "a",
919 make_attrs(&[
920 ("group", json!([uref(2)])),
921 ("lookup", json!({ "k": uref(3) })),
922 ]),
923 ),
924 create_node(2, "b", make_attrs(&[])),
925 create_node(3, "c", make_attrs(&[])),
926 ];
927 let sorted = sort_ops_for_apply(&ops, &schema);
928 let a = order_index(&sorted, 1);
929 assert!(order_index(&sorted, 2) < a, "ref nested in list");
930 assert!(order_index(&sorted, 3) < a, "ref nested in map");
931 }
932
933 #[test]
934 fn toposort_reference_cycle_falls_back_to_stable_order() {
935 let schema = node_schema(&[("next", ref_t())]);
936 let ops = vec![
937 create_node(1, "aaa", make_attrs(&[("next", uref(2))])),
938 create_node(2, "bbb", make_attrs(&[("next", uref(1))])),
939 ];
940 let sorted = sort_ops_for_apply(&ops, &schema);
941 assert_eq!(sorted.len(), 2);
943 assert_eq!(order_index(&sorted, 1), 0);
945 assert_eq!(order_index(&sorted, 2), 1);
946 }
947
948 #[test]
949 fn reverse_toposort_deletes_child_before_parent() {
950 let mut node_key = BTreeMap::new();
953 node_key.insert("slug".to_string(), field(FieldType::Slug));
954 let mut child_key = BTreeMap::new();
955 child_key.insert("parent".to_string(), field(ref_t()));
956 child_key.insert("slug".to_string(), field(FieldType::Slug));
957 let mut types = BTreeMap::new();
958 types.insert(
959 "node".to_string(),
960 TypeSchema {
961 key: node_key,
962 fields: BTreeMap::new(),
963 },
964 );
965 types.insert(
966 "child".to_string(),
967 TypeSchema {
968 key: child_key,
969 fields: BTreeMap::new(),
970 },
971 );
972 let schema = Schema { types };
973
974 let parent_key = Key::from(BTreeMap::from([("slug".to_string(), json!("p"))]));
975 let child_key_val = Key::from(BTreeMap::from([
976 ("parent".to_string(), uref(1)),
977 ("slug".to_string(), json!("c")),
978 ]));
979 let ops = vec![
980 Op::Delete {
981 uid: Uid::from_u128(1),
982 type_name: TypeName::new("node"),
983 key: parent_key,
984 backend_id: None,
985 },
986 Op::Delete {
987 uid: Uid::from_u128(2),
988 type_name: TypeName::new("child"),
989 key: child_key_val,
990 backend_id: None,
991 },
992 ];
993 let sorted = sort_ops_for_apply(&ops, &schema);
994 assert!(
995 order_index(&sorted, 2) < order_index(&sorted, 1),
996 "child deleted before the parent it references"
997 );
998 assert!(sorted.iter().all(|op| matches!(op, Op::Delete { .. })));
999 }
1000
1001 #[test]
1002 fn toposort_keeps_deletes_after_creates_and_updates() {
1003 let schema = node_schema(&[("next", ref_t())]);
1004 let ops = vec![
1005 Op::Delete {
1006 uid: Uid::from_u128(9),
1007 type_name: TypeName::new("node"),
1008 key: make_key("z"),
1009 backend_id: None,
1010 },
1011 create_node(1, "a", make_attrs(&[("next", uref(2))])),
1012 create_node(2, "b", make_attrs(&[])),
1013 ];
1014 let sorted = sort_ops_for_apply(&ops, &schema);
1015 let non_delete_count = sorted
1016 .iter()
1017 .filter(|op| !matches!(op, Op::Delete { .. }))
1018 .count();
1019 let delete_pos = sorted
1020 .iter()
1021 .position(|op| matches!(op, Op::Delete { .. }))
1022 .unwrap();
1023 assert_eq!(delete_pos, non_delete_count);
1025 assert!(order_index(&sorted, 2) < order_index(&sorted, 1));
1027 }
1028}