1use std::collections::{HashMap, HashSet};
18
19use panproto_gat::Name;
20
21use crate::fan::Fan;
22use crate::functor::FInstance;
23use crate::wtype::WInstance;
24
25#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
28#[non_exhaustive]
29pub enum HomError {
30 #[error("node map is not total: domain node {0} has no image")]
32 NodeNotMapped(u32),
33
34 #[error("node map sends domain node {src} to nonexistent codomain node {tgt}")]
36 ImageNodeMissing {
37 src: u32,
39 tgt: u32,
41 },
42
43 #[error(
46 "anchor not preserved: node {src} anchored at `{src_anchor}` \
47 maps to node {tgt} anchored at `{tgt_anchor}`"
48 )]
49 AnchorMismatch {
50 src: u32,
52 tgt: u32,
54 src_anchor: Name,
56 tgt_anchor: Name,
58 },
59
60 #[error(
63 "arc ({parent},{child}) is not preserved: \
64 no codomain arc ({mapped_parent},{mapped_child}) of the same edge"
65 )]
66 ArcNotPreserved {
67 parent: u32,
69 child: u32,
71 mapped_parent: u32,
73 mapped_child: u32,
75 },
76
77 #[error("fan `{hyper_edge_id}` at parent {parent} is not preserved")]
79 FanNotPreserved {
80 hyper_edge_id: String,
82 parent: u32,
84 },
85
86 #[error("attributes not preserved: node {src} and its image {tgt} carry different values")]
89 AttributeMismatch {
90 src: u32,
92 tgt: u32,
94 },
95
96 #[error("root not preserved: domain root {src} maps to {mapped}, but codomain root is {cod}")]
98 RootNotPreserved {
99 src: u32,
101 mapped: u32,
103 cod: u32,
105 },
106
107 #[error("row map for table `{table}` is missing or malformed: {detail}")]
110 RowMapMalformed {
111 table: String,
113 detail: String,
115 },
116
117 #[error(
119 "foreign key `{edge}` is not preserved: mapped pair \
120 ({mapped_src},{mapped_tgt}) absent from the codomain"
121 )]
122 ForeignKeyNotPreserved {
123 edge: String,
125 mapped_src: usize,
127 mapped_tgt: usize,
129 },
130
131 #[error("cannot compose: intermediate node {0} is outside the second map's domain")]
134 ComposeNodeMismatch(u32),
135
136 #[error("cannot compose F-instance homs on table `{table}`: {detail}")]
139 ComposeRowMismatch {
140 table: String,
142 detail: String,
144 },
145}
146
147#[derive(Clone, Debug, Default, PartialEq, Eq)]
153pub struct WInstanceHom {
154 pub node_map: HashMap<u32, u32>,
156}
157
158impl WInstanceHom {
159 #[must_use]
161 pub const fn new(node_map: HashMap<u32, u32>) -> Self {
162 Self { node_map }
163 }
164
165 #[must_use]
167 pub fn identity(instance: &WInstance) -> Self {
168 Self {
169 node_map: instance.nodes.keys().map(|&id| (id, id)).collect(),
170 }
171 }
172
173 pub fn check(&self, dom: &WInstance, cod: &WInstance) -> Result<(), HomError> {
185 for (&src, node) in &dom.nodes {
187 let &tgt = self
188 .node_map
189 .get(&src)
190 .ok_or(HomError::NodeNotMapped(src))?;
191 let image = cod
192 .nodes
193 .get(&tgt)
194 .ok_or(HomError::ImageNodeMissing { src, tgt })?;
195 if node.anchor != image.anchor {
196 return Err(HomError::AnchorMismatch {
197 src,
198 tgt,
199 src_anchor: node.anchor.clone(),
200 tgt_anchor: image.anchor.clone(),
201 });
202 }
203 if node.value != image.value || node.extra_fields != image.extra_fields {
207 return Err(HomError::AttributeMismatch { src, tgt });
208 }
209 }
210
211 let cod_arcs: HashSet<(u32, u32, &panproto_schema::Edge)> =
213 cod.arcs.iter().map(|(p, c, e)| (*p, *c, e)).collect();
214 for (parent, child, edge) in &dom.arcs {
215 let mapped_parent = self.node_map[parent];
216 let mapped_child = self.node_map[child];
217 if !cod_arcs.contains(&(mapped_parent, mapped_child, edge)) {
218 return Err(HomError::ArcNotPreserved {
219 parent: *parent,
220 child: *child,
221 mapped_parent,
222 mapped_child,
223 });
224 }
225 }
226
227 for fan in &dom.fans {
230 let mapped = Fan {
231 hyper_edge_id: fan.hyper_edge_id.clone(),
232 parent: self.node_map[&fan.parent],
233 children: fan
234 .children
235 .iter()
236 .map(|(label, id)| (label.clone(), self.node_map[id]))
237 .collect(),
238 };
239 if !cod.fans.contains(&mapped) {
240 return Err(HomError::FanNotPreserved {
241 hyper_edge_id: fan.hyper_edge_id.clone(),
242 parent: fan.parent,
243 });
244 }
245 }
246
247 let mapped_root = self.node_map[&dom.root];
249 if mapped_root != cod.root {
250 return Err(HomError::RootNotPreserved {
251 src: dom.root,
252 mapped: mapped_root,
253 cod: cod.root,
254 });
255 }
256
257 Ok(())
258 }
259
260 pub fn compose(&self, other: &Self) -> Result<Self, HomError> {
268 let mut node_map = HashMap::with_capacity(self.node_map.len());
269 for (&a, &b) in &self.node_map {
270 let &c = other
271 .node_map
272 .get(&b)
273 .ok_or(HomError::ComposeNodeMismatch(b))?;
274 node_map.insert(a, c);
275 }
276 Ok(Self { node_map })
277 }
278
279 #[must_use]
283 pub fn is_isomorphism(&self, dom: &WInstance, cod: &WInstance) -> bool {
284 if self.check(dom, cod).is_err() {
285 return false;
286 }
287 if self.node_map.len() != cod.nodes.len() {
289 return false;
290 }
291 let mut inverse = HashMap::with_capacity(self.node_map.len());
292 for (&src, &tgt) in &self.node_map {
293 if inverse.insert(tgt, src).is_some() {
295 return false;
296 }
297 }
298 Self { node_map: inverse }.check(cod, dom).is_ok()
301 }
302}
303
304#[derive(Clone, Debug, Default, PartialEq, Eq)]
310pub struct FInstanceHom {
311 pub row_maps: HashMap<String, Vec<usize>>,
314}
315
316impl FInstanceHom {
317 #[must_use]
319 pub const fn new(row_maps: HashMap<String, Vec<usize>>) -> Self {
320 Self { row_maps }
321 }
322
323 #[must_use]
325 pub fn identity(instance: &FInstance) -> Self {
326 Self {
327 row_maps: instance
328 .tables
329 .iter()
330 .map(|(name, rows)| (name.clone(), (0..rows.len()).collect()))
331 .collect(),
332 }
333 }
334
335 pub fn check(&self, dom: &FInstance, cod: &FInstance) -> Result<(), HomError> {
346 for (table, rows) in &dom.tables {
347 let map = self
348 .row_maps
349 .get(table)
350 .ok_or_else(|| HomError::RowMapMalformed {
351 table: table.clone(),
352 detail: "no row map for this table".to_owned(),
353 })?;
354 if map.len() != rows.len() {
355 return Err(HomError::RowMapMalformed {
356 table: table.clone(),
357 detail: format!(
358 "row map has {} entries but the table has {} rows",
359 map.len(),
360 rows.len()
361 ),
362 });
363 }
364 let cod_rows = cod.tables.get(table).map_or(&[][..], Vec::as_slice);
365 for (i, (&j, row)) in map.iter().zip(rows).enumerate() {
366 if j >= cod_rows.len() {
367 return Err(HomError::RowMapMalformed {
368 table: table.clone(),
369 detail: format!(
370 "image row {j} out of range (codomain has {} rows)",
371 cod_rows.len()
372 ),
373 });
374 }
375 if *row != cod_rows[j] {
377 return Err(HomError::AttributeMismatch {
378 src: u32::try_from(i).unwrap_or(u32::MAX),
379 tgt: u32::try_from(j).unwrap_or(u32::MAX),
380 });
381 }
382 }
383 }
384
385 for (edge, pairs) in &dom.foreign_keys {
386 let src_map =
387 self.row_maps
388 .get(edge.src.as_ref())
389 .ok_or_else(|| HomError::RowMapMalformed {
390 table: edge.src.to_string(),
391 detail: "foreign-key source table has no row map".to_owned(),
392 })?;
393 let tgt_map =
394 self.row_maps
395 .get(edge.tgt.as_ref())
396 .ok_or_else(|| HomError::RowMapMalformed {
397 table: edge.tgt.to_string(),
398 detail: "foreign-key target table has no row map".to_owned(),
399 })?;
400 let cod_pairs: HashSet<(usize, usize)> = cod
401 .foreign_keys
402 .get(edge)
403 .into_iter()
404 .flatten()
405 .copied()
406 .collect();
407 for &(si, ti) in pairs {
408 let mapped = (src_map[si], tgt_map[ti]);
409 if !cod_pairs.contains(&mapped) {
410 return Err(HomError::ForeignKeyNotPreserved {
411 edge: format!("{}->{}", edge.src, edge.tgt),
412 mapped_src: mapped.0,
413 mapped_tgt: mapped.1,
414 });
415 }
416 }
417 }
418
419 Ok(())
420 }
421
422 pub fn compose(&self, other: &Self) -> Result<Self, HomError> {
430 let mut row_maps = HashMap::with_capacity(self.row_maps.len());
431 for (table, map) in &self.row_maps {
432 let next = other
433 .row_maps
434 .get(table)
435 .ok_or_else(|| HomError::ComposeRowMismatch {
436 table: table.clone(),
437 detail: "second map has no row map for this table".to_owned(),
438 })?;
439 let mut composed = Vec::with_capacity(map.len());
440 for &j in map {
441 let &k = next.get(j).ok_or_else(|| HomError::ComposeRowMismatch {
442 table: table.clone(),
443 detail: format!("intermediate row {j} is outside the second map's domain"),
444 })?;
445 composed.push(k);
446 }
447 row_maps.insert(table.clone(), composed);
448 }
449 Ok(Self { row_maps })
450 }
451
452 #[must_use]
456 pub fn is_isomorphism(&self, dom: &FInstance, cod: &FInstance) -> bool {
457 if self.check(dom, cod).is_err() {
458 return false;
459 }
460 if self.row_maps.len() != cod.tables.len() {
461 return false;
462 }
463 let mut inverse = HashMap::with_capacity(self.row_maps.len());
464 for (table, map) in &self.row_maps {
465 let cod_rows = cod.tables.get(table).map_or(0, Vec::len);
466 if map.len() != cod_rows {
467 return false;
468 }
469 let mut inv = vec![usize::MAX; cod_rows];
470 for (i, &j) in map.iter().enumerate() {
471 if inv[j] != usize::MAX {
472 return false; }
474 inv[j] = i;
475 }
476 if inv.contains(&usize::MAX) {
477 return false; }
479 inverse.insert(table.clone(), inv);
480 }
481 Self { row_maps: inverse }.check(cod, dom).is_ok()
482 }
483}
484
485#[cfg(test)]
486#[allow(clippy::expect_used, clippy::unwrap_used)]
487mod tests {
488 use std::collections::HashMap;
489
490 use panproto_schema::Edge;
491
492 use super::*;
493 use crate::metadata::Node;
494 use crate::value::Value;
495
496 fn edge(src: &str, tgt: &str) -> Edge {
497 Edge {
498 src: src.into(),
499 tgt: tgt.into(),
500 kind: "prop".into(),
501 name: None,
502 }
503 }
504
505 fn w_pair(root_id: u32, child_id: u32) -> WInstance {
507 let mut nodes = HashMap::new();
508 nodes.insert(root_id, Node::new(root_id, "root"));
509 nodes.insert(child_id, Node::new(child_id, "leaf"));
510 WInstance::new(
511 nodes,
512 vec![(root_id, child_id, edge("root", "leaf"))],
513 vec![],
514 root_id,
515 "root".into(),
516 )
517 }
518
519 #[test]
520 fn identity_checks_and_composes() {
521 let inst = w_pair(0, 1);
522 let id = WInstanceHom::identity(&inst);
523 assert!(id.check(&inst, &inst).is_ok());
524
525 let renamed = w_pair(10, 11);
527 let h = WInstanceHom::new(HashMap::from([(0, 10), (1, 11)]));
528 assert!(h.check(&inst, &renamed).is_ok());
529
530 let id_cod = WInstanceHom::identity(&renamed);
532 let composed = h.compose(&id_cod).expect("compose with identity");
533 assert_eq!(composed, h);
534
535 assert!(h.is_isomorphism(&inst, &renamed));
536 }
537
538 #[test]
539 fn arc_breaking_node_map_fails_check() {
540 let dom = w_pair(0, 1);
542 let mut nodes = HashMap::new();
543 nodes.insert(0, Node::new(0, "root"));
544 nodes.insert(1, Node::new(1, "leaf"));
545 nodes.insert(2, Node::new(2, "leaf"));
546 let cod = WInstance::new(
548 nodes,
549 vec![(0, 2, edge("root", "leaf"))],
550 vec![],
551 0,
552 "root".into(),
553 );
554 let h = WInstanceHom::new(HashMap::from([(0, 0), (1, 1)]));
557 let err = h.check(&dom, &cod).expect_err("arc naturality must fail");
558 assert!(matches!(err, HomError::ArcNotPreserved { .. }));
559 }
560
561 #[test]
562 fn anchor_mismatch_fails_check() {
563 let dom = w_pair(0, 1);
564 let mut nodes = HashMap::new();
565 nodes.insert(0, Node::new(0, "root"));
566 nodes.insert(1, Node::new(1, "other"));
568 let cod = WInstance::new(
569 nodes,
570 vec![(0, 1, edge("root", "leaf"))],
571 vec![],
572 0,
573 "root".into(),
574 );
575 let h = WInstanceHom::identity(&dom);
576 let err = h.check(&dom, &cod).expect_err("anchor mismatch must fail");
577 assert!(matches!(err, HomError::AnchorMismatch { .. }));
578 }
579
580 #[test]
581 fn incompatible_compose_errors() {
582 let h1 = WInstanceHom::new(HashMap::from([(0, 0), (1, 99)]));
584 let h2 = WInstanceHom::new(HashMap::from([(0, 0), (1, 1)]));
585 let err = h1
586 .compose(&h2)
587 .expect_err("compose must reject dangling image");
588 assert_eq!(err, HomError::ComposeNodeMismatch(99));
589 }
590
591 #[test]
592 fn non_root_preserving_map_fails() {
593 let dom = w_pair(0, 1);
594 let renamed = w_pair(10, 11);
595 let h = WInstanceHom::new(HashMap::from([(0, 11), (1, 10)]));
597 let err = h.check(&dom, &renamed).expect_err("root must be preserved");
598 assert!(matches!(
600 err,
601 HomError::RootNotPreserved { .. } | HomError::AnchorMismatch { .. }
602 ));
603 }
604
605 fn f_pair() -> FInstance {
606 let posts = vec![
607 HashMap::from([("id".to_owned(), Value::Int(1))]),
608 HashMap::from([("id".to_owned(), Value::Int(2))]),
609 ];
610 let users = vec![HashMap::from([("id".to_owned(), Value::Int(10))])];
611 FInstance::new()
612 .with_table("post", posts)
613 .with_table("user", users)
614 .with_foreign_key(edge("post", "user"), vec![(0, 0), (1, 0)])
615 }
616
617 #[test]
618 fn finstance_identity_and_fk_naturality() {
619 let inst = f_pair();
620 let id = FInstanceHom::identity(&inst);
621 assert!(id.check(&inst, &inst).is_ok());
622 assert!(id.is_isomorphism(&inst, &inst));
623
624 let mut broken = f_pair();
628 broken
629 .foreign_keys
630 .insert(edge("post", "user"), vec![(0, 0)]);
631 let err = id
632 .check(&inst, &broken)
633 .expect_err("FK naturality must fail");
634 assert!(matches!(err, HomError::ForeignKeyNotPreserved { .. }));
635 }
636
637 #[test]
638 fn winstance_attribute_change_fails_check() {
639 let mut dom_nodes = HashMap::new();
641 dom_nodes.insert(0, Node::new(0, "root"));
642 dom_nodes.insert(
643 1,
644 Node::new(1, "leaf").with_extra_field("weight", Value::Int(1)),
645 );
646 let dom = WInstance::new(
647 dom_nodes,
648 vec![(0, 1, edge("root", "leaf"))],
649 vec![],
650 0,
651 "root".into(),
652 );
653 let mut cod_nodes = HashMap::new();
654 cod_nodes.insert(0, Node::new(0, "root"));
655 cod_nodes.insert(
656 1,
657 Node::new(1, "leaf").with_extra_field("weight", Value::Int(2)),
658 );
659 let cod = WInstance::new(
660 cod_nodes,
661 vec![(0, 1, edge("root", "leaf"))],
662 vec![],
663 0,
664 "root".into(),
665 );
666 let h = WInstanceHom::identity(&dom);
667 let err = h
668 .check(&dom, &cod)
669 .expect_err("differing attribute must fail check");
670 assert!(matches!(
671 err,
672 HomError::AttributeMismatch { src: 1, tgt: 1 }
673 ));
674 }
675
676 #[test]
677 fn finstance_attribute_change_fails_check() {
678 let inst = f_pair();
679 let id = FInstanceHom::identity(&inst);
680 let mut altered = f_pair();
683 altered.tables.get_mut("post").expect("post table")[0]
684 .insert("id".to_owned(), Value::Int(99));
685 let err = id
686 .check(&inst, &altered)
687 .expect_err("differing row must fail check");
688 assert!(matches!(err, HomError::AttributeMismatch { src: 0, .. }));
689 }
690}