1use std::collections::{HashMap, HashSet};
2
3use gen_core::{HashId, Workspace};
4use gen_models::{
5 block_group::BlockGroup, changesets::ChangesetModels, db::OperationsConnection,
6 errors::OperationError, operations::Operation, session_operations::DependencyModels,
7 traits::Query,
8};
9use petgraph::Direction;
10use thiserror::Error;
11
12use crate::graph::{DiffGenGraph, get_diff_graph};
13
14#[derive(Debug, Error)]
15pub enum OperationDiffError {
16 #[error("No current operation is checked out.")]
17 NoCurrentOperation,
18 #[error("Operation {0} not found.")]
19 OperationMissing(HashId),
20 #[error("Unable to find path between {0} and {1}.")]
21 PathNotFound(HashId, HashId),
22 #[error("Missing changeset data for operation {0}.")]
23 MissingChangeset(HashId),
24 #[error(transparent)]
25 OperationError(#[from] OperationError),
26}
27
28#[derive(Clone, Debug)]
29pub struct BlockGroupDiff {
30 pub id: HashId,
31 pub block_group: Option<BlockGroup>,
32 pub graph: DiffGenGraph,
33}
34
35#[derive(Clone, Debug)]
36pub struct OperationDiff {
37 pub operations: Vec<HashId>,
38 pub dbs: HashMap<String, DbDiff>,
39}
40
41#[derive(Clone, Debug)]
42pub struct DbDiff {
43 pub db_path: String,
44 pub added_block_groups: Vec<BlockGroupDiff>,
45 pub removed_block_groups: Vec<BlockGroupDiff>,
46}
47
48#[derive(Clone, Debug, Default)]
49pub struct BlockGroupDiffs {
50 pub operations: Vec<HashId>,
51 pub block_group_diffs: Vec<BlockGroupDiff>,
52}
53
54fn build_operation_diffs(
55 operations_in_order: &[HashId],
56 added_graphs: &HashMap<String, BlockGroupDiffs>,
57 removed_graphs: &HashMap<String, BlockGroupDiffs>,
58) -> HashMap<String, OperationDiff> {
59 let mut db_paths = HashSet::new();
60 db_paths.extend(added_graphs.keys().cloned());
61 db_paths.extend(removed_graphs.keys().cloned());
62
63 let mut diffs = HashMap::new();
64 for db_path in db_paths {
65 let mut op_set = HashSet::new();
66 if let Some(diffs) = added_graphs.get(&db_path) {
67 op_set.extend(diffs.operations.iter().copied());
68 }
69 if let Some(diffs) = removed_graphs.get(&db_path) {
70 op_set.extend(diffs.operations.iter().copied());
71 }
72 let operations = operations_in_order
73 .iter()
74 .copied()
75 .filter(|hash| op_set.contains(hash))
76 .collect::<Vec<_>>();
77 let db_diff = DbDiff {
78 db_path: db_path.clone(),
79 added_block_groups: added_graphs
80 .get(&db_path)
81 .map(|diffs| diffs.block_group_diffs.clone())
82 .unwrap_or_default(),
83 removed_block_groups: removed_graphs
84 .get(&db_path)
85 .map(|diffs| diffs.block_group_diffs.clone())
86 .unwrap_or_default(),
87 };
88 let mut dbs = HashMap::new();
89 dbs.insert(db_path.clone(), db_diff);
90 diffs.insert(db_path, OperationDiff { operations, dbs });
91 }
92
93 diffs
94}
95
96pub fn collect_operation_diff(
97 workspace: &Workspace,
98 op_conn: &OperationsConnection,
99 from_hash: Option<HashId>,
100 to_hash: HashId,
101 db_path: Option<&str>,
102) -> Result<HashMap<String, OperationDiff>, OperationDiffError> {
103 let (operations_in_order, added_ops, removed_ops) = if let Some(from_hash) = from_hash {
104 if from_hash == to_hash {
105 return Ok(HashMap::new());
106 }
107
108 let path = Operation::get_path_between(op_conn, from_hash, to_hash);
109 if path.is_empty() {
110 return Err(OperationDiffError::PathNotFound(from_hash, to_hash));
111 }
112
113 let mut operations_in_order = vec![];
114 let mut added_ops = vec![];
115 let mut removed_ops = vec![];
116 for (src, direction, dest) in path {
117 let op_hash = match direction {
118 Direction::Outgoing => dest,
119 Direction::Incoming => src,
120 };
121 operations_in_order.push(op_hash);
122 match direction {
123 Direction::Outgoing => {
124 added_ops.push(op_hash);
125 }
126 Direction::Incoming => {
127 removed_ops.push(op_hash);
128 }
129 }
130 }
131
132 (operations_in_order, added_ops, removed_ops)
133 } else {
134 let upstream_operations = Operation::get_upstream(op_conn, &to_hash);
135 (upstream_operations.clone(), upstream_operations, vec![])
136 };
137
138 let added_graphs = build_block_group_diffs(workspace, op_conn, &added_ops, db_path)?;
139 let removed_graphs = build_block_group_diffs(workspace, op_conn, &removed_ops, db_path)?;
140
141 Ok(build_operation_diffs(
142 &operations_in_order,
143 &added_graphs,
144 &removed_graphs,
145 ))
146}
147
148fn build_block_group_diffs(
152 workspace: &Workspace,
153 op_conn: &OperationsConnection,
154 operations: &[HashId],
155 db_path: Option<&str>,
156) -> Result<HashMap<String, BlockGroupDiffs>, OperationDiffError> {
157 if operations.is_empty() {
158 return Ok(HashMap::new());
159 }
160
161 #[derive(Default)]
162 struct DbAccumulator {
163 operations: Vec<HashId>,
164 block_group_info: HashMap<HashId, BlockGroup>,
165 block_groups: HashSet<gen_models::block_group::BlockGroup>,
166 edges: HashSet<gen_models::edge::Edge>,
167 block_group_edges: HashSet<gen_models::block_group_edge::BlockGroupEdge>,
168 nodes: HashSet<gen_models::node::Node>,
169 sequences: HashSet<gen_models::sequence::Sequence>,
170 dep_edges: HashSet<gen_models::edge::Edge>,
171 dep_nodes: HashSet<gen_models::node::Node>,
172 dep_sequences: HashSet<gen_models::sequence::Sequence>,
173 }
174
175 let mut accumulators: HashMap<String, DbAccumulator> = HashMap::new();
176
177 for op_hash in operations {
178 let operation = Operation::get_by_id(op_conn, op_hash)
179 .ok_or_else(|| OperationDiffError::OperationMissing(*op_hash))?;
180 let changeset = operation.get_changeset(workspace);
181 if let Some(db_path) = db_path
182 && changeset.db_path != db_path
183 {
184 continue;
185 }
186 let changeset_db = changeset.db_path.clone();
187 let changeset = changeset.changes;
188 let dependencies = operation.get_changeset_dependencies(workspace);
189
190 let entry = accumulators.entry(changeset_db).or_default();
191 entry.operations.push(*op_hash);
192
193 for block_group in changeset
194 .block_groups
195 .iter()
196 .chain(dependencies.block_group.iter())
197 {
198 entry
199 .block_group_info
200 .entry(block_group.id)
201 .or_insert_with(|| block_group.clone());
202 }
203
204 entry.block_groups.extend(changeset.block_groups);
205 entry.edges.extend(changeset.edges);
206 entry.block_group_edges.extend(changeset.block_group_edges);
207 entry.nodes.extend(changeset.nodes);
208 entry.sequences.extend(changeset.sequences);
209 entry.dep_edges.extend(dependencies.edges);
210 entry.dep_nodes.extend(dependencies.nodes);
211 entry.dep_sequences.extend(dependencies.sequences);
212 }
213
214 let mut results = HashMap::new();
215 for (db_path, acc) in accumulators {
216 let merged_graphs = get_diff_graph(
217 &ChangesetModels {
218 block_groups: acc.block_groups.into_iter().collect(),
219 edges: acc.edges.into_iter().collect(),
220 block_group_edges: acc.block_group_edges.into_iter().collect(),
221 nodes: acc.nodes.into_iter().collect(),
222 sequences: acc.sequences.into_iter().collect(),
223 ..Default::default()
224 },
225 &DependencyModels {
226 edges: acc.dep_edges.into_iter().collect(),
227 nodes: acc.dep_nodes.into_iter().collect(),
228 sequences: acc.dep_sequences.into_iter().collect(),
229 ..Default::default()
230 },
231 );
232
233 let mut block_groups = merged_graphs
234 .into_iter()
235 .map(|(id, graph)| {
236 let block_group = acc.block_group_info.get(&id).cloned();
237 BlockGroupDiff {
238 id,
239 block_group,
240 graph,
241 }
242 })
243 .collect::<Vec<_>>();
244 block_groups.sort_by_key(|a| {
245 if let Some(bg) = &a.block_group {
246 (
247 bg.collection_name.clone(),
248 bg.sample_name.clone(),
249 bg.name.clone(),
250 format!("{id}", id = a.id),
251 )
252 } else {
253 (
254 String::new(),
255 String::new(),
256 String::new(),
257 format!("{id}", id = a.id),
258 )
259 }
260 });
261 results.insert(
262 db_path,
263 BlockGroupDiffs {
264 operations: acc.operations,
265 block_group_diffs: block_groups,
266 },
267 );
268 }
269
270 Ok(results)
271}
272
273#[cfg(test)]
274mod tests {
275 use gen_core::{HashId, Strand};
276 use gen_models::{
277 block_group::BlockGroup,
278 block_group_edge::BlockGroupEdge,
279 changesets::{ChangesetModels, DatabaseChangeset, write_changeset},
280 edge::Edge,
281 node::Node,
282 operations::{Branch, Operation, OperationState},
283 sequence::{NewSequence, Sequence},
284 };
285
286 use super::*;
287 use crate::test_helpers::setup_gen;
288
289 fn get_db_diff<'a>(diffs: &'a HashMap<String, OperationDiff>, db_path: &str) -> &'a DbDiff {
290 diffs
291 .get(db_path)
292 .and_then(|diff| diff.dbs.get(db_path))
293 .expect("db diff")
294 }
295
296 fn base_dependencies(start_node: &Node, end_node: &Node) -> DependencyModels {
297 let mut start_sequence = Sequence::new()
298 .sequence_type("DNA")
299 .sequence("")
300 .name("start")
301 .build();
302 start_sequence.hash = start_node.sequence_hash;
303 let mut end_sequence = Sequence::new()
304 .sequence_type("DNA")
305 .sequence("")
306 .name("end")
307 .build();
308 end_sequence.hash = end_node.sequence_hash;
309 DependencyModels {
310 collections: vec![],
311 samples: vec![],
312 sequences: vec![start_sequence, end_sequence],
313 block_group: vec![],
314 nodes: vec![start_node.clone(), end_node.clone()],
315 edges: vec![],
316 paths: vec![],
317 accessions: vec![],
318 accession_edges: vec![],
319 }
320 }
321
322 fn simple_changeset(
323 block_group: &BlockGroup,
324 node: &Node,
325 seq: &Sequence,
326 start_node: &Node,
327 end_node: &Node,
328 ) -> (ChangesetModels, DependencyModels) {
329 let edges = vec![
330 Edge {
331 id: HashId::convert_str(&format!("{}-{}-start", block_group.id, node.id)),
332 source_node_id: start_node.id,
333 source_coordinate: 0,
334 source_strand: Strand::Forward,
335 target_node_id: node.id,
336 target_coordinate: 0,
337 target_strand: Strand::Forward,
338 },
339 Edge {
340 id: HashId::convert_str(&format!("{}-{}-end", block_group.id, node.id)),
341 source_node_id: node.id,
342 source_coordinate: seq.length,
343 source_strand: Strand::Forward,
344 target_node_id: end_node.id,
345 target_coordinate: 0,
346 target_strand: Strand::Forward,
347 },
348 ];
349 let block_group_edges = vec![
350 BlockGroupEdge {
351 id: HashId::convert_str(&format!("{}-{}-start-bge", block_group.id, node.id)),
352 block_group_id: block_group.id,
353 edge_id: edges[0].id,
354 chromosome_index: 0,
355 phased: 0,
356 created_on: 0,
357 },
358 BlockGroupEdge {
359 id: HashId::convert_str(&format!("{}-{}-end-bge", block_group.id, node.id)),
360 block_group_id: block_group.id,
361 edge_id: edges[1].id,
362 chromosome_index: 0,
363 phased: 0,
364 created_on: 0,
365 },
366 ];
367 let changeset = ChangesetModels {
368 collections: vec![],
369 samples: vec![],
370 sample_lineages: vec![],
371 sequences: vec![seq.clone()],
372 block_groups: vec![block_group.clone()],
373 nodes: vec![node.clone()],
374 edges,
375 block_group_edges,
376 paths: vec![],
377 path_edges: vec![],
378 accessions: vec![],
379 accession_edges: vec![],
380 accession_paths: vec![],
381 annotation_groups: vec![],
382 annotations: vec![],
383 annotation_group_samples: vec![],
384 };
385 let dependencies = base_dependencies(start_node, end_node);
386 (changeset, dependencies)
387 }
388
389 #[test]
390 fn one_operation_diff() {
391 let context = setup_gen();
392 let op_conn = context.operations().conn();
393 let workspace = context.workspace();
394 let start_node = Node::get_start_node();
395 let end_node = Node::get_end_node();
396
397 let base_op =
398 Operation::create(op_conn, "seed", &HashId::pad_str(1)).expect("create base op");
399
400 let seq_one = NewSequence::new()
401 .sequence_type("dna")
402 .sequence("AAAAA")
403 .name("one")
404 .build();
405 let node_one = Node {
406 id: HashId::pad_str(10),
407 sequence_hash: seq_one.hash,
408 };
409 let block_group = BlockGroup {
410 id: HashId::pad_str(3),
411 collection_name: "c".to_string(),
412 sample_name: "s".to_string(),
413 name: "bg".to_string(),
414 created_on: 0,
415 parent_block_group_id: None,
416 is_default: false,
417 };
418
419 let head = Operation::create(op_conn, "add", &HashId::pad_str(2)).expect("create op");
420 let (changeset, dependencies) =
421 simple_changeset(&block_group, &node_one, &seq_one, &start_node, &end_node);
422 write_changeset(
423 workspace,
424 &head,
425 DatabaseChangeset {
426 db_path: "diff.db".to_string(),
427 changes: changeset,
428 },
429 &dependencies,
430 );
431
432 let diffs = collect_operation_diff(workspace, op_conn, Some(base_op.hash), head.hash, None)
433 .expect("diff");
434 let diff = diffs.get("diff.db").expect("diff db");
435 let db_diff = get_db_diff(&diffs, "diff.db");
436 assert_eq!(diff.operations, vec![head.hash]);
437 assert_eq!(db_diff.added_block_groups.len(), 1);
438 assert!(db_diff.removed_block_groups.is_empty());
439 let graph = &db_diff.added_block_groups[0].graph;
440 assert_eq!(graph.nodes().count(), 3);
441 assert_eq!(graph.all_edges().count(), 2);
442 }
443
444 #[test]
445 fn initial_operation_diff_contains_added_block_groups() {
446 let context = setup_gen();
447 let op_conn = context.operations().conn();
448 let workspace = context.workspace();
449 let start_node = Node::get_start_node();
450 let end_node = Node::get_end_node();
451
452 let seq_one = NewSequence::new()
453 .sequence_type("dna")
454 .sequence("AAAAA")
455 .name("one")
456 .build();
457 let node_one = Node {
458 id: HashId::pad_str(10),
459 sequence_hash: seq_one.hash,
460 };
461 let block_group = BlockGroup {
462 id: HashId::pad_str(3),
463 collection_name: "c".to_string(),
464 sample_name: "s".to_string(),
465 name: "bg".to_string(),
466 created_on: 0,
467 parent_block_group_id: None,
468 is_default: false,
469 };
470
471 let head = Operation::create(op_conn, "add", &HashId::pad_str(2)).expect("create op");
472 let (changeset, dependencies) =
473 simple_changeset(&block_group, &node_one, &seq_one, &start_node, &end_node);
474 write_changeset(
475 workspace,
476 &head,
477 DatabaseChangeset {
478 db_path: "diff.db".to_string(),
479 changes: changeset,
480 },
481 &dependencies,
482 );
483
484 let diffs =
485 collect_operation_diff(workspace, op_conn, None, head.hash, None).expect("diff");
486 let diff = diffs.get("diff.db").expect("diff db");
487 let db_diff = get_db_diff(&diffs, "diff.db");
488 assert_eq!(diff.operations, vec![head.hash]);
489 assert_eq!(db_diff.added_block_groups.len(), 1);
490 assert!(db_diff.removed_block_groups.is_empty());
491 let graph = &db_diff.added_block_groups[0].graph;
492 assert_eq!(graph.nodes().count(), 3);
493 assert_eq!(graph.all_edges().count(), 2);
494 }
495
496 #[test]
497 fn merges_multiple_operations() {
498 let context = setup_gen();
499 let op_conn = context.operations().conn();
500 let workspace = context.workspace();
501 let start_node = Node::get_start_node();
502 let end_node = Node::get_end_node();
503
504 let op1 = Operation::create(op_conn, "seed", &HashId::pad_str(1)).expect("create base op");
505
506 let bg_one = BlockGroup {
507 id: HashId::pad_str(3),
508 collection_name: "c".to_string(),
509 sample_name: "s".to_string(),
510 name: "bg1".to_string(),
511 created_on: 0,
512 parent_block_group_id: None,
513 is_default: false,
514 };
515 let seq_one = NewSequence::new()
516 .sequence_type("dna")
517 .sequence("AAAAA")
518 .name("one")
519 .build();
520 let node_one = Node {
521 id: HashId::pad_str(10),
522 sequence_hash: seq_one.hash,
523 };
524 let op2 = Operation::create(op_conn, "add", &HashId::pad_str(2)).expect("create op2");
525 let (changeset_one, dependencies_one) =
526 simple_changeset(&bg_one, &node_one, &seq_one, &start_node, &end_node);
527 write_changeset(
528 workspace,
529 &op2,
530 DatabaseChangeset {
531 db_path: "diff.db".to_string(),
532 changes: changeset_one,
533 },
534 &dependencies_one,
535 );
536
537 let bg_two = BlockGroup {
538 id: HashId::pad_str(4),
539 collection_name: "c".to_string(),
540 sample_name: "s".to_string(),
541 name: "bg2".to_string(),
542 created_on: 0,
543 parent_block_group_id: None,
544 is_default: false,
545 };
546 let seq_two = NewSequence::new()
547 .sequence_type("dna")
548 .sequence("CCCCC")
549 .name("two")
550 .build();
551 let node_two = Node {
552 id: HashId::pad_str(11),
553 sequence_hash: seq_two.hash,
554 };
555 let op3 = Operation::create(op_conn, "add", &HashId::pad_str(3)).expect("create op3");
556 let (changeset_two, dependencies_two) =
557 simple_changeset(&bg_two, &node_two, &seq_two, &start_node, &end_node);
558 write_changeset(
559 workspace,
560 &op3,
561 DatabaseChangeset {
562 db_path: "diff.db".to_string(),
563 changes: changeset_two,
564 },
565 &dependencies_two,
566 );
567
568 let diffs = collect_operation_diff(workspace, op_conn, Some(op1.hash), op3.hash, None)
569 .expect("diff");
570 let diff = diffs.get("diff.db").expect("diff db");
571 let db_diff = get_db_diff(&diffs, "diff.db");
572 assert_eq!(diff.operations, vec![op2.hash, op3.hash]);
573 assert_eq!(db_diff.added_block_groups.len(), 2);
574 }
575
576 #[test]
577 fn diff_against_itself_is_empty() {
578 let context = setup_gen();
579 let op_conn = context.operations().conn();
580 let workspace = context.workspace();
581 let base = Operation::create(op_conn, "seed", &HashId::pad_str(1)).expect("create base op");
582 let diffs = collect_operation_diff(workspace, op_conn, Some(base.hash), base.hash, None)
583 .expect("diff");
584 assert!(diffs.is_empty());
585 }
586
587 #[test]
588 fn diffs_across_branches() {
589 let context = setup_gen();
590 let op_conn = context.operations().conn();
591 let workspace = context.workspace();
592 let start_node = Node::get_start_node();
593 let end_node = Node::get_end_node();
594
595 let base = Operation::create(op_conn, "seed", &HashId::pad_str(1)).expect("base op");
596
597 let main_block_group = BlockGroup {
598 id: HashId::pad_str(20),
599 collection_name: "c".to_string(),
600 sample_name: "s".to_string(),
601 name: "main".to_string(),
602 created_on: 0,
603 parent_block_group_id: None,
604 is_default: false,
605 };
606 let main_seq = NewSequence::new()
607 .sequence_type("dna")
608 .sequence("AAAAA")
609 .name("main")
610 .build();
611 let main_node = Node {
612 id: HashId::pad_str(21),
613 sequence_hash: main_seq.hash,
614 };
615 let op_main = Operation::create(op_conn, "add", &HashId::pad_str(2)).expect("main op");
616 let (main_changeset, main_deps) = simple_changeset(
617 &main_block_group,
618 &main_node,
619 &main_seq,
620 &start_node,
621 &end_node,
622 );
623 write_changeset(
624 workspace,
625 &op_main,
626 DatabaseChangeset {
627 db_path: "diff.db".to_string(),
628 changes: main_changeset,
629 },
630 &main_deps,
631 );
632
633 let feature_branch = Branch::create_with_remote(op_conn, "feature", None).unwrap();
634 OperationState::set_branch(op_conn, &feature_branch.name);
635 OperationState::set_operation(op_conn, &base.hash);
636
637 let feature_block_group = BlockGroup {
638 id: HashId::pad_str(30),
639 collection_name: "c".to_string(),
640 sample_name: "s".to_string(),
641 name: "feature".to_string(),
642 created_on: 0,
643 parent_block_group_id: None,
644 is_default: false,
645 };
646 let feature_seq = NewSequence::new()
647 .sequence_type("dna")
648 .sequence("CCCCC")
649 .name("feature")
650 .build();
651 let feature_node = Node {
652 id: HashId::pad_str(31),
653 sequence_hash: feature_seq.hash,
654 };
655 let op_feature =
656 Operation::create(op_conn, "add", &HashId::pad_str(3)).expect("feature op");
657 let (feature_changeset, feature_deps) = simple_changeset(
658 &feature_block_group,
659 &feature_node,
660 &feature_seq,
661 &start_node,
662 &end_node,
663 );
664 write_changeset(
665 workspace,
666 &op_feature,
667 DatabaseChangeset {
668 db_path: "diff.db".to_string(),
669 changes: feature_changeset,
670 },
671 &feature_deps,
672 );
673
674 let diffs = collect_operation_diff(
675 workspace,
676 op_conn,
677 Some(op_main.hash),
678 op_feature.hash,
679 None,
680 )
681 .expect("diff");
682 let diff = diffs.get("diff.db").expect("diff db");
683 let db_diff = get_db_diff(&diffs, "diff.db");
684 assert_eq!(diff.operations, vec![op_main.hash, op_feature.hash]);
685 assert_eq!(db_diff.added_block_groups.len(), 1);
686 assert_eq!(db_diff.removed_block_groups.len(), 1);
687 assert_eq!(db_diff.added_block_groups[0].id, feature_block_group.id);
688 assert_eq!(db_diff.removed_block_groups[0].id, main_block_group.id);
689 }
690
691 #[test]
692 fn filters_by_database_path() {
693 let context = setup_gen();
694 let op_conn = context.operations().conn();
695 let workspace = context.workspace();
696 let start_node = Node::get_start_node();
697 let end_node = Node::get_end_node();
698
699 let base = Operation::create(op_conn, "seed", &HashId::pad_str(1)).expect("base op");
700
701 let block_group_one = BlockGroup {
702 id: HashId::pad_str(40),
703 collection_name: "c".to_string(),
704 sample_name: "s".to_string(),
705 name: "db-one".to_string(),
706 created_on: 0,
707 parent_block_group_id: None,
708 is_default: false,
709 };
710 let seq_one = NewSequence::new()
711 .sequence_type("dna")
712 .sequence("AAAAA")
713 .name("one")
714 .build();
715 let node_one = Node {
716 id: HashId::pad_str(41),
717 sequence_hash: seq_one.hash,
718 };
719 let op_one = Operation::create(op_conn, "add", &HashId::pad_str(2)).expect("op one");
720 let (changeset_one, deps_one) = simple_changeset(
721 &block_group_one,
722 &node_one,
723 &seq_one,
724 &start_node,
725 &end_node,
726 );
727 write_changeset(
728 workspace,
729 &op_one,
730 DatabaseChangeset {
731 db_path: "db-one.db".to_string(),
732 changes: changeset_one,
733 },
734 &deps_one,
735 );
736
737 let block_group_two = BlockGroup {
738 id: HashId::pad_str(50),
739 collection_name: "c".to_string(),
740 sample_name: "s".to_string(),
741 name: "db-two".to_string(),
742 created_on: 0,
743 parent_block_group_id: None,
744 is_default: false,
745 };
746 let seq_two = NewSequence::new()
747 .sequence_type("dna")
748 .sequence("CCCCC")
749 .name("two")
750 .build();
751 let node_two = Node {
752 id: HashId::pad_str(51),
753 sequence_hash: seq_two.hash,
754 };
755 let op_two = Operation::create(op_conn, "add", &HashId::pad_str(3)).expect("op two");
756 let (changeset_two, deps_two) = simple_changeset(
757 &block_group_two,
758 &node_two,
759 &seq_two,
760 &start_node,
761 &end_node,
762 );
763 write_changeset(
764 workspace,
765 &op_two,
766 DatabaseChangeset {
767 db_path: "db-two.db".to_string(),
768 changes: changeset_two,
769 },
770 &deps_two,
771 );
772
773 let diffs = collect_operation_diff(
774 workspace,
775 op_conn,
776 Some(base.hash),
777 op_two.hash,
778 Some("db-one.db"),
779 )
780 .expect("diff");
781 let diff = diffs.get("db-one.db").expect("diff db");
782 let db_diff = get_db_diff(&diffs, "db-one.db");
783 assert_eq!(diff.operations, vec![op_one.hash]);
784 assert_eq!(db_diff.added_block_groups.len(), 1);
785 assert_eq!(db_diff.added_block_groups[0].id, block_group_one.id);
786
787 let diff_none = collect_operation_diff(
788 workspace,
789 op_conn,
790 Some(base.hash),
791 op_two.hash,
792 Some("missing.db"),
793 )
794 .expect("diff");
795 assert!(diff_none.is_empty());
796 }
797}