Skip to main content

panproto_inst/
acset.rs

1//! Trait for attributed C-set operations shared across all instance shapes.
2//!
3//! [`AcsetOps`] provides a unified interface for restrict, extend, and
4//! introspection operations on the three instance shapes:
5//! [`WInstance`](crate::WInstance), [`FInstance`](crate::FInstance),
6//! and [`GInstance`](crate::GInstance).
7
8use panproto_schema::Schema;
9
10use crate::error::RestrictError;
11use crate::functor::FInstance;
12use crate::ginstance::GInstance;
13use crate::wtype::{CompiledMigration, WInstance};
14
15/// Trait for attributed C-set operations shared across all instance shapes.
16pub trait AcsetOps: Clone + std::fmt::Debug {
17    /// Restrict this instance along a compiled migration.
18    ///
19    /// Corresponds to `Delta_F` (precomposition / pullback).
20    ///
21    /// # Errors
22    ///
23    /// Returns [`RestrictError`] if the restrict pipeline fails.
24    fn restrict(
25        &self,
26        src_schema: &Schema,
27        tgt_schema: &Schema,
28        migration: &CompiledMigration,
29    ) -> Result<Self, RestrictError>;
30
31    /// Extend (left Kan extension, `Sigma_F`) this instance along a migration.
32    ///
33    /// # Errors
34    ///
35    /// Returns [`RestrictError`] if the extend pipeline fails.
36    fn extend(
37        &self,
38        tgt_schema: &Schema,
39        migration: &CompiledMigration,
40    ) -> Result<Self, RestrictError>;
41
42    /// Returns the number of top-level elements (nodes, rows, or vertices).
43    fn element_count(&self) -> usize;
44
45    /// Returns the shape name as a static string.
46    fn shape_name(&self) -> &'static str;
47}
48
49impl AcsetOps for WInstance {
50    fn restrict(
51        &self,
52        src_schema: &Schema,
53        tgt_schema: &Schema,
54        migration: &CompiledMigration,
55    ) -> Result<Self, RestrictError> {
56        crate::wtype::wtype_restrict(self, src_schema, tgt_schema, migration)
57    }
58
59    fn extend(
60        &self,
61        tgt_schema: &Schema,
62        migration: &CompiledMigration,
63    ) -> Result<Self, RestrictError> {
64        crate::wtype::wtype_extend(self, tgt_schema, migration)
65    }
66
67    fn element_count(&self) -> usize {
68        self.node_count()
69    }
70
71    fn shape_name(&self) -> &'static str {
72        "wtype"
73    }
74}
75
76impl AcsetOps for FInstance {
77    fn restrict(
78        &self,
79        _src_schema: &Schema,
80        _tgt_schema: &Schema,
81        migration: &CompiledMigration,
82    ) -> Result<Self, RestrictError> {
83        crate::functor::functor_restrict(self, migration)
84    }
85
86    fn extend(
87        &self,
88        _tgt_schema: &Schema,
89        migration: &CompiledMigration,
90    ) -> Result<Self, RestrictError> {
91        crate::functor::functor_extend(self, migration)
92    }
93
94    fn element_count(&self) -> usize {
95        self.table_count()
96    }
97
98    fn shape_name(&self) -> &'static str {
99        "functor"
100    }
101}
102
103impl AcsetOps for GInstance {
104    fn restrict(
105        &self,
106        _src_schema: &Schema,
107        _tgt_schema: &Schema,
108        migration: &CompiledMigration,
109    ) -> Result<Self, RestrictError> {
110        crate::ginstance::graph_restrict(self, migration)
111    }
112
113    fn extend(
114        &self,
115        _tgt_schema: &Schema,
116        migration: &CompiledMigration,
117    ) -> Result<Self, RestrictError> {
118        crate::ginstance::graph_extend(self, migration)
119    }
120
121    fn element_count(&self) -> usize {
122        self.node_count()
123    }
124
125    fn shape_name(&self) -> &'static str {
126        "graph"
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use std::collections::{HashMap, HashSet};
133
134    use panproto_gat::Name;
135    use panproto_schema::Edge;
136
137    use super::*;
138    use crate::metadata::Node;
139    use crate::value::{FieldPresence, Value};
140
141    // -----------------------------------------------------------------------
142    // Helpers
143    // -----------------------------------------------------------------------
144
145    fn make_empty_schema() -> Schema {
146        Schema {
147            protocol: "test".into(),
148            vertices: HashMap::new(),
149            edges: HashMap::new(),
150            hyper_edges: HashMap::new(),
151            constraints: HashMap::new(),
152            required: HashMap::new(),
153            nsids: HashMap::new(),
154            entries: Vec::new(),
155            variants: HashMap::new(),
156            orderings: HashMap::new(),
157            recursion_points: HashMap::new(),
158            spans: HashMap::new(),
159            usage_modes: HashMap::new(),
160            nominal: HashMap::new(),
161            coercions: HashMap::new(),
162            mergers: HashMap::new(),
163            defaults: HashMap::new(),
164            policies: HashMap::new(),
165            outgoing: HashMap::new(),
166            incoming: HashMap::new(),
167            between: HashMap::new(),
168        }
169    }
170
171    fn make_test_schema(vertices: &[&str], edges: &[Edge]) -> Schema {
172        use smallvec::smallvec;
173        let mut between = HashMap::new();
174        for edge in edges {
175            between
176                .entry((Name::from(&*edge.src), Name::from(&*edge.tgt)))
177                .or_insert_with(|| smallvec![])
178                .push(edge.clone());
179        }
180        Schema {
181            protocol: "test".into(),
182            vertices: vertices
183                .iter()
184                .map(|&v| {
185                    (
186                        Name::from(v),
187                        panproto_schema::Vertex {
188                            id: Name::from(v),
189                            kind: Name::from("object"),
190                            nsid: None,
191                        },
192                    )
193                })
194                .collect(),
195            edges: HashMap::new(),
196            hyper_edges: HashMap::new(),
197            constraints: HashMap::new(),
198            required: HashMap::new(),
199            nsids: HashMap::new(),
200            entries: Vec::new(),
201            variants: HashMap::new(),
202            orderings: HashMap::new(),
203            recursion_points: HashMap::new(),
204            spans: HashMap::new(),
205            usage_modes: HashMap::new(),
206            nominal: HashMap::new(),
207            coercions: HashMap::new(),
208            mergers: HashMap::new(),
209            defaults: HashMap::new(),
210            policies: HashMap::new(),
211            outgoing: HashMap::new(),
212            incoming: HashMap::new(),
213            between,
214        }
215    }
216
217    fn identity_migration(verts: &[&str], edges: &[Edge]) -> CompiledMigration {
218        CompiledMigration {
219            surviving_verts: verts.iter().map(|&v| Name::from(v)).collect(),
220            surviving_edges: edges.iter().cloned().collect(),
221            vertex_remap: HashMap::new(),
222            edge_remap: HashMap::new(),
223            resolver: HashMap::new(),
224            hyper_resolver: HashMap::new(),
225            field_transforms: HashMap::new(),
226            conditional_survival: HashMap::new(),
227            expansion_path: HashMap::new(),
228        }
229    }
230
231    fn three_node_winstance() -> WInstance {
232        let mut nodes = HashMap::new();
233        nodes.insert(0, Node::new(0, "post:body"));
234        nodes.insert(
235            1,
236            Node::new(1, "post:body.text")
237                .with_value(FieldPresence::Present(Value::Str("hello".into()))),
238        );
239        nodes.insert(
240            2,
241            Node::new(2, "post:body.createdAt")
242                .with_value(FieldPresence::Present(Value::Str("2024-01-01".into()))),
243        );
244        let arcs = vec![
245            (
246                0,
247                1,
248                Edge {
249                    src: "post:body".into(),
250                    tgt: "post:body.text".into(),
251                    kind: "prop".into(),
252                    name: Some("text".into()),
253                },
254            ),
255            (
256                0,
257                2,
258                Edge {
259                    src: "post:body".into(),
260                    tgt: "post:body.createdAt".into(),
261                    kind: "prop".into(),
262                    name: Some("createdAt".into()),
263                },
264            ),
265        ];
266        WInstance::new(nodes, arcs, vec![], 0, Name::from("post:body"))
267    }
268
269    fn two_node_finstance() -> FInstance {
270        let mut row = HashMap::new();
271        row.insert("name".to_string(), Value::Str("Alice".into()));
272        FInstance::new().with_table("users", vec![row])
273    }
274
275    fn two_node_ginstance() -> (GInstance, Edge) {
276        let edge = Edge {
277            src: "person".into(),
278            tgt: "person".into(),
279            kind: "knows".into(),
280            name: None,
281        };
282        let g = GInstance::new()
283            .with_node(Node::new(0, "person"))
284            .with_node(Node::new(1, "person"))
285            .with_edge(0, 1, edge.clone())
286            .with_value(0, Value::Str("Alice".into()))
287            .with_value(1, Value::Str("Bob".into()));
288        (g, edge)
289    }
290
291    // -----------------------------------------------------------------------
292    // shape_name tests
293    // -----------------------------------------------------------------------
294
295    #[test]
296    fn winstance_shape_name() {
297        let w = three_node_winstance();
298        assert_eq!(AcsetOps::shape_name(&w), "wtype");
299    }
300
301    #[test]
302    fn finstance_shape_name() {
303        let f = two_node_finstance();
304        assert_eq!(AcsetOps::shape_name(&f), "functor");
305    }
306
307    #[test]
308    fn ginstance_shape_name() {
309        let (g, _) = two_node_ginstance();
310        assert_eq!(AcsetOps::shape_name(&g), "graph");
311    }
312
313    // -----------------------------------------------------------------------
314    // element_count tests
315    // -----------------------------------------------------------------------
316
317    #[test]
318    fn winstance_element_count() {
319        let w = three_node_winstance();
320        assert_eq!(AcsetOps::element_count(&w), 3);
321    }
322
323    #[test]
324    fn finstance_element_count() {
325        let f = two_node_finstance();
326        assert_eq!(AcsetOps::element_count(&f), 1);
327    }
328
329    #[test]
330    fn ginstance_element_count() {
331        let (g, _) = two_node_ginstance();
332        assert_eq!(AcsetOps::element_count(&g), 2);
333    }
334
335    // -----------------------------------------------------------------------
336    // restrict through trait matches direct function call
337    // -----------------------------------------------------------------------
338
339    #[test]
340    fn winstance_restrict_via_trait() -> Result<(), Box<dyn std::error::Error>> {
341        let w = three_node_winstance();
342        let edge_text = Edge {
343            src: "post:body".into(),
344            tgt: "post:body.text".into(),
345            kind: "prop".into(),
346            name: Some("text".into()),
347        };
348        let tgt_schema = make_test_schema(&["post:body", "post:body.text"], &[edge_text]);
349        let src_schema = make_empty_schema();
350        let migration = CompiledMigration {
351            surviving_verts: HashSet::from([Name::from("post:body"), Name::from("post:body.text")]),
352            surviving_edges: HashSet::new(),
353            vertex_remap: HashMap::new(),
354            edge_remap: HashMap::new(),
355            resolver: HashMap::new(),
356            hyper_resolver: HashMap::new(),
357            field_transforms: HashMap::new(),
358            conditional_survival: HashMap::new(),
359            expansion_path: HashMap::new(),
360        };
361
362        let via_trait = AcsetOps::restrict(&w, &src_schema, &tgt_schema, &migration)?;
363        let via_fn = crate::wtype::wtype_restrict(&w, &src_schema, &tgt_schema, &migration)?;
364        assert_eq!(via_trait.node_count(), via_fn.node_count());
365        assert_eq!(via_trait.arc_count(), via_fn.arc_count());
366        Ok(())
367    }
368
369    #[test]
370    fn finstance_restrict_via_trait() -> Result<(), Box<dyn std::error::Error>> {
371        let f = two_node_finstance();
372        let schema = make_empty_schema();
373        let migration = identity_migration(&["users"], &[]);
374
375        let via_trait = AcsetOps::restrict(&f, &schema, &schema, &migration)?;
376        let via_fn = crate::functor::functor_restrict(&f, &migration)?;
377        assert_eq!(via_trait.table_count(), via_fn.table_count());
378        Ok(())
379    }
380
381    #[test]
382    fn ginstance_restrict_via_trait() -> Result<(), Box<dyn std::error::Error>> {
383        let (g, _edge) = two_node_ginstance();
384        let schema = make_empty_schema();
385        let migration = CompiledMigration {
386            surviving_verts: HashSet::from([Name::from("person_new")]),
387            surviving_edges: HashSet::new(),
388            vertex_remap: HashMap::from([("person".into(), "person_new".into())]),
389            edge_remap: HashMap::new(),
390            resolver: HashMap::new(),
391            hyper_resolver: HashMap::new(),
392            field_transforms: HashMap::new(),
393            conditional_survival: HashMap::new(),
394            expansion_path: HashMap::new(),
395        };
396
397        let via_trait = AcsetOps::restrict(&g, &schema, &schema, &migration)?;
398        let via_fn = crate::ginstance::graph_restrict(&g, &migration)?;
399        assert_eq!(via_trait.node_count(), via_fn.node_count());
400        assert_eq!(via_trait.edge_count(), via_fn.edge_count());
401        Ok(())
402    }
403
404    // -----------------------------------------------------------------------
405    // graph_extend tests
406    // -----------------------------------------------------------------------
407
408    #[test]
409    fn graph_extend_identity_preserves_instance() -> Result<(), Box<dyn std::error::Error>> {
410        let (g, edge) = two_node_ginstance();
411        let schema = make_empty_schema();
412        let migration = identity_migration(&["person"], &[edge]);
413
414        let result = AcsetOps::extend(&g, &schema, &migration)?;
415        assert_eq!(result.node_count(), 2);
416        assert_eq!(result.edge_count(), 1);
417        assert_eq!(result.values.len(), 2);
418        assert_eq!(result.values[&0], Value::Str("Alice".into()));
419        assert_eq!(result.values[&1], Value::Str("Bob".into()));
420        Ok(())
421    }
422
423    #[test]
424    fn graph_extend_vertex_remap_updates_anchors() -> Result<(), Box<dyn std::error::Error>> {
425        let (g, edge) = two_node_ginstance();
426        let schema = make_empty_schema();
427        let new_edge = Edge {
428            src: "human".into(),
429            tgt: "human".into(),
430            kind: "knows".into(),
431            name: None,
432        };
433        let migration = CompiledMigration {
434            surviving_verts: HashSet::from([Name::from("human")]),
435            surviving_edges: HashSet::new(),
436            vertex_remap: HashMap::from([("person".into(), "human".into())]),
437            edge_remap: HashMap::from([(edge, new_edge.clone())]),
438            resolver: HashMap::new(),
439            hyper_resolver: HashMap::new(),
440            field_transforms: HashMap::new(),
441            conditional_survival: HashMap::new(),
442            expansion_path: HashMap::new(),
443        };
444
445        let result = AcsetOps::extend(&g, &schema, &migration)?;
446        assert_eq!(result.node_count(), 2);
447        assert_eq!(result.nodes[&0].anchor, Name::from("human"));
448        assert_eq!(result.nodes[&1].anchor, Name::from("human"));
449        assert_eq!(result.edge_count(), 1);
450        assert_eq!(result.edges[0].2, new_edge);
451        // Values preserved
452        assert_eq!(result.values[&0], Value::Str("Alice".into()));
453        Ok(())
454    }
455}