panproto_inst/complement.rs
1//! The complement: data discarded when projecting an instance forward.
2//!
3//! A [`Complement`] records everything a forward projection (a lens `get` or a
4//! `restrict_with_complement`) drops from a source [`WInstance`](crate::WInstance),
5//! so that the backward direction can reconstruct the original source from a
6//! (possibly modified) view. It is the shared complement type for both the
7//! asymmetric lens in `panproto-lens` and the polynomial-functor restrict
8//! pipeline in [`crate::poly`].
9//!
10//! Not every field is populated by every producer: the lens `get` path fills
11//! the structural-reconstruction fields (dropped nodes/arcs/fans, contraction
12//! choices, parent map, arc-edge disambiguators, snapshots, synthesized
13//! nodes, source fingerprint); the restrict pipeline additionally records
14//! [`Complement::contracted_into`], the surviving ancestor each dropped node
15//! collapsed into. Unused fields stay empty and are omitted from the
16//! serialized form.
17
18use std::collections::{HashMap, HashSet};
19
20use panproto_schema::Edge;
21
22use crate::fan::Fan;
23use crate::metadata::Node;
24use crate::value::{FieldPresence, Value};
25
26/// The complement: data discarded by a forward projection, needed by the
27/// backward direction to restore the original source instance.
28///
29/// When a forward projection maps a source instance to a target view, some
30/// nodes, arcs, and structural decisions are lost. The complement records all
31/// of this so the backward direction can reconstruct the full source.
32#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
33pub struct Complement {
34 /// Nodes from the source that do not appear in the target view.
35 pub dropped_nodes: HashMap<u32, Node>,
36 /// Arcs from the source that do not appear in the target view.
37 pub dropped_arcs: Vec<(u32, u32, Edge)>,
38 /// Fans from the source whose parent or children were dropped.
39 pub dropped_fans: Vec<Fan>,
40 /// Resolver decisions made during ancestor contraction.
41 pub contraction_choices: HashMap<(u32, u32), Edge>,
42 /// Original parent mapping before contraction.
43 pub original_parent: HashMap<u32, u32>,
44 /// Fingerprint of the source schema at projection time, used by the
45 /// backward direction to validate that the complement matches the lens's
46 /// source schema.
47 #[serde(default)]
48 pub source_fingerprint: u64,
49 /// Pre-transform `extra_fields` for nodes that had `field_transforms`
50 /// applied. Used by the backward direction to restore original field
51 /// values.
52 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
53 pub original_extra_fields: HashMap<u32, HashMap<String, Value>>,
54 /// Exact edge used for every arc in the view, keyed by
55 /// `(parent_id, child_id)`. This makes the backward direction
56 /// deterministic when the source schema has parallel edges between the
57 /// same vertex pair, ensuring the cartesian lift is unique.
58 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
59 pub arc_edges: HashMap<(u32, u32), Edge>,
60 /// Pre-coercion `node.value` for nodes that had `__value__` field
61 /// transforms applied. Used by the backward direction to restore the
62 /// original leaf value.
63 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
64 pub original_values: HashMap<u32, Option<FieldPresence>>,
65 /// View node ids synthesized during forward evaluation by the nest-style
66 /// `expansion_path` mechanism. These nodes exist in the view (to satisfy
67 /// the target schema's multi-hop path) but have no counterpart in the
68 /// source instance. The backward direction drops them when reconstructing
69 /// the source.
70 #[serde(default, skip_serializing_if = "HashSet::is_empty")]
71 pub synthesized_nodes: HashSet<u32>,
72 /// For each dropped node, the surviving node it contracted into (its
73 /// nearest surviving ancestor). Populated by the restrict pipeline's
74 /// ancestor contraction; the fiber of a target node is its direct
75 /// preimage together with every source node recorded here as contracted
76 /// into it.
77 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
78 pub contracted_into: HashMap<u32, u32>,
79}
80
81impl Complement {
82 /// Create an empty complement (no data discarded).
83 #[must_use]
84 pub fn empty() -> Self {
85 Self::default()
86 }
87
88 /// Returns `true` if the complement discards no data (lossless
89 /// transformation).
90 ///
91 /// The [`source_fingerprint`](Self::source_fingerprint) is provenance
92 /// metadata rather than discarded data, so a lossless projection that
93 /// still records a fingerprint counts as empty.
94 #[must_use]
95 pub fn is_empty(&self) -> bool {
96 self.dropped_nodes.is_empty()
97 && self.dropped_arcs.is_empty()
98 && self.dropped_fans.is_empty()
99 && self.contraction_choices.is_empty()
100 && self.original_parent.is_empty()
101 && self.original_extra_fields.is_empty()
102 && self.arc_edges.is_empty()
103 && self.original_values.is_empty()
104 && self.synthesized_nodes.is_empty()
105 && self.contracted_into.is_empty()
106 }
107}